diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3780e6a7ddf..abf60d63a45 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,7 +55,7 @@ /storagecontrol/**/* @GoogleCloudPlatform/gcs-sdk-team @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /storagetransfer/**/* @GoogleCloudPlatform/gcs-sdk-team @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers # ---* Infra DB -/alloydb/**/* @GoogleCloudPlatform/alloydb-connectors-code-owners @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers +/alloydb/**/* @GoogleCloudPlatform/alloydb-connectors-code-owners @GoogleCloudPlatform/alloydb-vector-search-docs @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /cloud-sql/**/* @GoogleCloudPlatform/cloud-sql-connectors @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers # Self-service @@ -73,19 +73,23 @@ # ---* Fully Eng Owned /aml-ai/**/* @GoogleCloudPlatform/aml-ai @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers +/alm/**/* @GoogleCloudPlatform/alm @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /appengine/**/* @GoogleCloudPlatform/serverless-runtimes @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /appengine/standard_python3/spanner/* @GoogleCloudPlatform/api-spanner-python @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /asset/**/* @GoogleCloudPlatform/cloud-asset-analysis-team @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /bigquery/**/* @chalmerlowe @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers +/bigquery/bigframes/**/* @tswast @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/cloud-samples-reviewers /bigquery/remote_function/**/* @autoerr @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /cloud-media-livestream/**/* @GoogleCloudPlatform/cloud-media-team @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /bigquery-connection/**/* @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /bigquery-datatransfer/**/* @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /bigquery-migration/**/* @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /bigquery-reservation/**/* @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers +/bigquery_storage/**/* @GoogleCloudPlatform/bigquery-readapi-team @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /connectgateway/**/* @GoogleCloudPlatform/connectgateway @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /dlp/**/* @GoogleCloudPlatform/googleapis-dlp @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /functions/spanner/* @GoogleCloudPlatform/api-spanner-python @GoogleCloudPlatform/functions-framework-google @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers +/geminidataanalytics/**/* @GoogleCloudPlatform/data-engineering-agent-team @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /healthcare/**/* @GoogleCloudPlatform/healthcare-life-sciences @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /retail/**/* @GoogleCloudPlatform/cloud-retail-team @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /billing/**/* @GoogleCloudPlatform/billing-samples-maintainers @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 35d67129a15..8b2e88470eb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,15 +2,17 @@ Fixes # -Note: Before submitting a pull request, please open an issue for discussion if you are not associated with Google. - ## Checklist -- [ ] I have followed [Sample Guidelines from AUTHORING_GUIDE.MD](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md) -- [ ] README is updated to include [all relevant information](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md#readme-file) -- [ ] **Tests** pass: `nox -s py-3.9` (see [Test Environment Setup](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md#test-environment-setup)) -- [ ] **Lint** pass: `nox -s lint` (see [Test Environment Setup](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md#test-environment-setup)) -- [ ] These samples need a new **API enabled** in testing projects to pass (let us know which ones) -- [ ] These samples need a new/updated **env vars** in testing projects set to pass (let us know which ones) -- [ ] This sample adds a new sample directory, and I updated the [CODEOWNERS file](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/.github/CODEOWNERS) with the codeowners for this sample -- [ ] This sample adds a new **Product API**, and I updated the [Blunderbuss issue/PR auto-assigner](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/.github/blunderbuss.yml) with the codeowners for this sample -- [ ] Please **merge** this PR for me once it is approved \ No newline at end of file + +### Testing +- [ ] **I have tested this change on a live environment and verified it works as intended.** + +### Compliance & Style +- [ ] I have followed the [Sample Guidelines](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md). +- [ ] The README is updated with [all relevant information](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md#readme-file) (if applicable). +- [ ] **If this PR adds a new sample directory:** I updated the [CODEOWNERS file](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/.github/CODEOWNERS). + +--- + +## Post-Approval Actions +- [ ] Please **merge** this PR for me once it is approved 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/scripts/lint-changed.sh b/.github/scripts/lint-changed.sh new file mode 100644 index 00000000000..0597909b9ae --- /dev/null +++ b/.github/scripts/lint-changed.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# 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. + +set -euo pipefail + +EVENT_NAME="${1:-local}" +BASE_REF="${2:-main}" + +echo "Configuring target diff for event: $EVENT_NAME" + +# Determine the target reference to diff against +if [ "$EVENT_NAME" = "pull_request" ]; then + echo "Fetching origin/$BASE_REF metadata..." + # Ensure we have the target branch metadata fetched. + git fetch origin "$BASE_REF" --depth=1 --quiet + + # Isolate to only changes in the PR + DIFF_COMMAND="origin/$BASE_REF..." +else + # Local fallback + # diff against local main branch. + echo "Running locally. Diffing against local $BASE_REF..." + DIFF_COMMAND="$BASE_REF" +fi + +# Gather modified/added Python files, explicitly ignoring deleted files via --diff-filter=d +DIFF_OUTPUT=$(git diff --name-only --diff-filter=d "$DIFF_COMMAND" -- '*.py' 2>/dev/null || true) + +if [ -n "$DIFF_OUTPUT" ]; then + mapfile -t CHANGED_FILES <<< "$DIFF_OUTPUT" +else + CHANGED_FILES=() +fi + +# Execute linters if changed Python files exist +if [ ${#CHANGED_FILES[@]} -gt 0 ]; then + echo "Files to lint:" + printf ' - %s\n' "${CHANGED_FILES[@]}" + + # Track execution success manually so both tools get a chance to run. + # This prevents the workflow from dying on Black without showing Flake8 errors. + BLACK_EXIT=0 + LINT_EXIT=0 + + # adding -q to silence some of the extraneous logging + echo "Running blacken..." + nox -s blacken -- "${CHANGED_FILES[@]}" || BLACK_EXIT=$? + + echo "Running flake8 lint..." + nox -s lint -- "${CHANGED_FILES[@]}" || LINT_EXIT=$? + + if [ $BLACK_EXIT -ne 0 ] || [ $LINT_EXIT -ne 0 ]; then + echo "❌ One or more linting checks failed." + exit 1 + fi +else + echo "✅ No Python files changed in this scope. Skipping checks." +fi diff --git a/.github/scripts/noxfile-lint.py b/.github/scripts/noxfile-lint.py new file mode 100644 index 00000000000..c1ce614150d --- /dev/null +++ b/.github/scripts/noxfile-lint.py @@ -0,0 +1,102 @@ +# 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. + +from __future__ import annotations + +import os + +import nox + +# Use a stable Python version for running the style utilities +LINTING_VERSION = "3.10" + +# Error out if the runner is missing the target python interpreter +nox.options.error_on_missing_interpreters = True + + +def _determine_local_import_names(start_dir: str) -> list[str]: + """Determines local import names to assist Flake8 with import order checks.""" + try: + 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 != "__pycache__") + ] + except Exception: + return [] + +# Linting with flake8. +# +# We ignore the following rules: +# ANN101: missing type annotation for `self` in method +# ANN102: missing type annotation for `cls` in method +# 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'] +# +# For more information see: https://pypi.org/project/flake8-annotations + +# Standardize style configuration parameters +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=ANN101,ANN102,E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session(python=LINTING_VERSION) +def lint(session: nox.sessions.Session) -> None: + """Runs flake8 linting checks. Honors incremental PR file arguments.""" + session.install("flake8", "flake8-import-order") + + local_names = _determine_local_import_names(".") + args = FLAKE8_COMMON_ARGS + [ + "--application-import-names", + ",".join(local_names), + ] + + if session.posargs: + args.extend(session.posargs) + else: + args.append(".") + + session.run("flake8", *args) + + +@nox.session(python=LINTING_VERSION) +def blacken(session: nox.sessions.Session) -> None: + """Runs black code formatting checks. Honors incremental PR file arguments.""" + session.install("black") + + # If explicit target files are passed via posargs, target ONLY those files. + if session.posargs: + targets = session.posargs + else: + # Fallback to scanning immediate root Python files if run purely locally without args + targets = [path for path in os.listdir(".") if path.endswith(".py")] + + if targets: + session.run("black", *targets) + else: + session.log("No specific Python targets identified for formatting validations.") diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index bf76c480c47..908068914cc 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -42,21 +42,10 @@ branchProtectionRules: requiresStrictStatusChecks: false # 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" + - "lint" - "cla/google" - "snippet-bot check" # List of explicit permissions to add (additive only) permissionRules: - # Team slug to add to repository permissions - - team: yoshi-admins - # Access level required, one of push|pull|admin - permission: admin - - team: yoshi-python-admins - permission: admin - - team: yoshi-python - permission: push - team: python-samples-owners permission: admin diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000000..9701c2f0de8 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +name: Lint + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.10' + + - name: Install nox + run: | + python -m pip install --upgrade pip + python -m pip install nox + + - name: Prepare noxfile + # copy from customized noxfile so that we don't lint everything in the repository + # we don't expect contributors to fix past lint issues + run: cp .github/scripts/noxfile-lint.py noxfile.py + + - name: Make script executable + run: chmod +x .github/scripts/lint-changed.sh + + - name: Run lint script for changed files + run: | + .github/scripts/lint-changed.sh "pull_request" "${{ github.base_ref }}" 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/appengine/flexible_python37_and_earlier/django_cloudsql/manage.py b/.internal/tests/fixtures/test_pass.py old mode 100755 new mode 100644 similarity index 66% rename from appengine/flexible_python37_and_earlier/django_cloudsql/manage.py rename to .internal/tests/fixtures/test_pass.py index 89fb5ae5607..e9cbf0f0225 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/manage.py +++ b/.internal/tests/fixtures/test_pass.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -# Copyright 2015 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. @@ -13,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys +# test_pass.py -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - from django.core.management import execute_from_command_line +def calculate_square(number: int) -> int: + """Calculates the square of a given integer.""" + result = number * number + print(f"The result is: {result}") + return result + - execute_from_command_line(sys.argv) +if __name__ == "__main__": + # Ensure a basic execution works cleanly + calculate_square(5) 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/lint/continuous.cfg b/.kokoro/lint/continuous.cfg.disabled similarity index 100% rename from .kokoro/lint/continuous.cfg rename to .kokoro/lint/continuous.cfg.disabled diff --git a/.kokoro/lint/periodic.cfg b/.kokoro/lint/periodic.cfg.disabled similarity index 100% rename from .kokoro/lint/periodic.cfg rename to .kokoro/lint/periodic.cfg.disabled diff --git a/.kokoro/lint/presubmit.cfg b/.kokoro/lint/presubmit.cfg.disabled similarity index 100% rename from .kokoro/lint/presubmit.cfg rename to .kokoro/lint/presubmit.cfg.disabled diff --git a/.kokoro/python3.10/continuous.cfg b/.kokoro/python3.10/continuous.cfg.disabled similarity index 100% rename from .kokoro/python3.10/continuous.cfg rename to .kokoro/python3.10/continuous.cfg.disabled diff --git a/.kokoro/python3.10/periodic.cfg b/.kokoro/python3.10/periodic.cfg.disabled similarity index 100% rename from .kokoro/python3.10/periodic.cfg rename to .kokoro/python3.10/periodic.cfg.disabled diff --git a/.kokoro/python3.10/presubmit.cfg b/.kokoro/python3.10/presubmit.cfg.disabled similarity index 100% rename from .kokoro/python3.10/presubmit.cfg rename to .kokoro/python3.10/presubmit.cfg.disabled diff --git a/.kokoro/python3.11/continuous.cfg b/.kokoro/python3.11/continuous.cfg.disabled similarity index 100% rename from .kokoro/python3.11/continuous.cfg rename to .kokoro/python3.11/continuous.cfg.disabled diff --git a/.kokoro/python3.11/periodic.cfg b/.kokoro/python3.11/periodic.cfg.disabled similarity index 100% rename from .kokoro/python3.11/periodic.cfg rename to .kokoro/python3.11/periodic.cfg.disabled diff --git a/.kokoro/python3.11/presubmit.cfg b/.kokoro/python3.11/presubmit.cfg.disabled similarity index 100% rename from .kokoro/python3.11/presubmit.cfg rename to .kokoro/python3.11/presubmit.cfg.disabled diff --git a/.kokoro/python3.12/continuous.cfg b/.kokoro/python3.12/continuous.cfg.disabled similarity index 100% rename from .kokoro/python3.12/continuous.cfg rename to .kokoro/python3.12/continuous.cfg.disabled diff --git a/.kokoro/python3.12/periodic.cfg b/.kokoro/python3.12/periodic.cfg.disabled similarity index 100% rename from .kokoro/python3.12/periodic.cfg rename to .kokoro/python3.12/periodic.cfg.disabled diff --git a/.kokoro/python3.12/presubmit.cfg b/.kokoro/python3.12/presubmit.cfg.disabled similarity index 100% rename from .kokoro/python3.12/presubmit.cfg rename to .kokoro/python3.12/presubmit.cfg.disabled diff --git a/.kokoro/python3.13/continuous.cfg b/.kokoro/python3.13/continuous.cfg.disabled similarity index 100% rename from .kokoro/python3.13/continuous.cfg rename to .kokoro/python3.13/continuous.cfg.disabled diff --git a/.kokoro/python3.13/periodic.cfg b/.kokoro/python3.13/periodic.cfg.disabled similarity index 100% rename from .kokoro/python3.13/periodic.cfg rename to .kokoro/python3.13/periodic.cfg.disabled diff --git a/.kokoro/python3.13/presubmit.cfg b/.kokoro/python3.13/presubmit.cfg.disabled similarity index 100% rename from .kokoro/python3.13/presubmit.cfg rename to .kokoro/python3.13/presubmit.cfg.disabled 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/python3.9/continuous.cfg b/.kokoro/python3.14/continuous.cfg.disabled similarity index 96% rename from .kokoro/python3.9/continuous.cfg rename to .kokoro/python3.14/continuous.cfg.disabled index cfbe29058c8..5753c38482a 100644 --- a/.kokoro/python3.9/continuous.cfg +++ b/.kokoro/python3.14/continuous.cfg.disabled @@ -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.disabled similarity index 81% rename from .kokoro/python2.7/periodic.cfg rename to .kokoro/python3.14/periodic.cfg.disabled index 1921dd0a999..8a14abb05ef 100644 --- a/.kokoro/python2.7/periodic.cfg +++ b/.kokoro/python3.14/periodic.cfg.disabled @@ -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/python3.9/presubmit.cfg b/.kokoro/python3.14/presubmit.cfg.disabled similarity index 96% rename from .kokoro/python3.9/presubmit.cfg rename to .kokoro/python3.14/presubmit.cfg.disabled index d74d307bbed..b8ecd3b0d15 100644 --- a/.kokoro/python3.9/presubmit.cfg +++ b/.kokoro/python3.14/presubmit.cfg.disabled @@ -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/python2.7/continuous.cfg b/.kokoro/python3.8/continuous.cfg.disabled similarity index 100% rename from .kokoro/python2.7/continuous.cfg rename to .kokoro/python3.8/continuous.cfg.disabled diff --git a/.kokoro/python3.8/periodic.cfg b/.kokoro/python3.8/periodic.cfg.disabled similarity index 100% rename from .kokoro/python3.8/periodic.cfg rename to .kokoro/python3.8/periodic.cfg.disabled diff --git a/.kokoro/python2.7/presubmit.cfg b/.kokoro/python3.8/presubmit.cfg.disabled similarity index 100% rename from .kokoro/python2.7/presubmit.cfg rename to .kokoro/python3.8/presubmit.cfg.disabled diff --git a/.kokoro/python3.8/continuous.cfg b/.kokoro/python3.9/continuous.cfg.disabled similarity index 100% rename from .kokoro/python3.8/continuous.cfg rename to .kokoro/python3.9/continuous.cfg.disabled diff --git a/.kokoro/python3.9/periodic.cfg b/.kokoro/python3.9/periodic.cfg.disabled similarity index 100% rename from .kokoro/python3.9/periodic.cfg rename to .kokoro/python3.9/periodic.cfg.disabled diff --git a/.kokoro/python3.8/presubmit.cfg b/.kokoro/python3.9/presubmit.cfg.disabled similarity index 100% rename from .kokoro/python3.8/presubmit.cfg rename to .kokoro/python3.9/presubmit.cfg.disabled 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/.trampolinerc b/.trampolinerc index ea532d7ea51..e90bf04b201 100644 --- a/.trampolinerc +++ b/.trampolinerc @@ -50,7 +50,7 @@ if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then fi if [[ -z "${RUN_TESTS_SESSION:-}" ]]; then - RUN_TESTS_SESSION="py-3.7" + RUN_TESTS_SESSION="py-3.10" fi if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then diff --git a/AUTHORING_GUIDE.md b/AUTHORING_GUIDE.md index abf5e31abf1..6ae8d0a0372 100644 --- a/AUTHORING_GUIDE.md +++ b/AUTHORING_GUIDE.md @@ -68,7 +68,7 @@ We recommend using the Python version management tool [Pyenv](https://github.com/pyenv/pyenv) if you are using MacOS or Linux. **Googlers:** See [the internal Python policies -doc](https://g3doc.corp.google.com/company/teams/cloud-devrel/dpe/samples/python.md?cl=head). +doc](go/cloudsamples/language-guides/python). **Using MacOS?:** See [Setting up a Mac development environment with pyenv and pyenv-virtualenv](MAC_SETUP.md). 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/alloydb/notebooks/alloydb_scann_benchmark_glove.ipynb b/alloydb/notebooks/alloydb_scann_benchmark_glove.ipynb new file mode 100644 index 00000000000..dd37f46fc4d --- /dev/null +++ b/alloydb/notebooks/alloydb_scann_benchmark_glove.ipynb @@ -0,0 +1,2012 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "id": "9WxWMjxAwH1e", + "cell_type": "code", + "source": [ + "# @title ⚙️ Benchmark Execution\n", + "# ==========================================\n", + "# 1. CONFIGURATION\n", + "# ==========================================\n", + "# @markdown ### **⚙️ 1. Cluster Configuration**\n", + "project_id = \"\" # @param {type:\"string\", placeholder:\"Project ID\"}\n", + "region = \"\" # @param {type:\"string\", placeholder:\"Region (e.g. us-central1)\"}\n", + "cluster_id = \"\" # @param {type:\"string\", placeholder:\"Cluster name\"}\n", + "instance_id = \"\" # @param {type:\"string\", placeholder:\"Instance Name\"}\n", + "\n", + "# @markdown ### **🔐 2. Database Credentials**\n", + "# @markdown *Note: Password will be requested securely when the benchmark is executed.*\n", + "db_user = \"postgres\" # @param {type:\"string\", placeholder:\"DB User\"}\n", + "db_name = \"postgres\" # @param {type:\"string\", placeholder:\"DB Name\"}\n", + "\n", + "# @markdown ### **⚡ 3. Benchmark Parameters**\n", + "# @markdown * `num_queries`: Total test queries to run.\n", + "# @markdown * `top_k`: Number of nearest neighbors to retrieve (LIMIT).\n", + "num_queries = 100 # @param [100, 500, 1000, 10000] {type:\"raw\"}\n", + "top_k = 10 # @param [10, 20, 50, 100] {type:\"raw\"}\n", + "\n", + "# ==========================================\n", + "# 2. SETUP & AUTHENTICATION\n", + "# ==========================================\n", + "import getpass\n", + "import os\n", + "import shutil\n", + "import sys\n", + "import time\n", + "import urllib.request\n", + "\n", + "# Prompt for password upfront so user doesn't wait for pip installation\n", + "db_pass = getpass.getpass(\"🔑 Enter Database Password for AlloyDB: \")\n", + "\n", + "# Install dependencies silently (standard for Colab VM runtimes)\n", + "try:\n", + " import pg8000\n", + " import google.cloud.alloydb.connector\n", + " import h5py\n", + " import matplotlib\n", + " import tqdm\n", + "except ImportError:\n", + " try:\n", + " # Run pip install in standard Colab environment\n", + " get_ipython().system(\n", + " \"pip install -q pg8000 google-cloud-alloydb-connector[pg8000] tqdm\"\n", + " \" matplotlib h5py numpy\"\n", + " )\n", + " except Exception as e:\n", + " print(f\"⚠️ Could not auto-install packages: {e}\")\n", + " print(\n", + " \"ℹ️ Ensure you are connected to a standard Colab runtime (e.g. on\"\n", + " \" colab.sandbox.google.com or colab.research.google.com).\"\n", + " )\n", + "\n", + "# Authenticate Colab session\n", + "try:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + "except Exception as e:\n", + " print(f\"ℹ️ Google Cloud auth note: {e}\")\n", + "\n", + "from IPython.display import clear_output, display, HTML\n", + "\n", + "clear_output()\n", + "\n", + "# ==========================================\n", + "# 3. LATE IMPORTS (Post-Installation) & THEME-ADAPTIVE STYLING\n", + "# ==========================================\n", + "import h5py\n", + "import matplotlib.patheffects as patheffects\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.ticker as ticker\n", + "import numpy as np\n", + "from tqdm.notebook import tqdm\n", + "from google.cloud.alloydb.connector import Connector\n", + "\n", + "\n", + "def print_step(step_label, text=\"\", is_success=False, is_error=False):\n", + " \"\"\"Renders status messages with styling tuned for both light and dark backgrounds.\"\"\"\n", + " base_style = \"font-size: 16px; margin-top: 10px; margin-bottom: 4px;\"\n", + " if is_success:\n", + " display(\n", + " HTML(f\"\"\"\n", + "
\n", + " {step_label} {text}\n", + "
\n", + " \"\"\")\n", + " )\n", + " elif is_error:\n", + " display(\n", + " HTML(f\"\"\"\n", + "
\n", + " {step_label} {text}\n", + "
\n", + " \"\"\")\n", + " )\n", + " else:\n", + " display(\n", + " HTML(f\"\"\"\n", + "
\n", + " {step_label}\n", + " {text}\n", + "
\n", + " \"\"\")\n", + " )\n", + "\n", + "\n", + "# ==========================================\n", + "# 4. DATASET CONFIGURATION (GloVe-100 Angular)\n", + "# ==========================================\n", + "DATASET_CONFIG = {\n", + " \"url\": \"https://ann-benchmarks.com/glove-100-angular.hdf5\",\n", + " \"file_name\": \"glove-100-angular.hdf5\",\n", + " \"dim\": 100,\n", + " \"table_name\": \"glove\",\n", + " \"test_table_name\": \"glove_test\",\n", + " \"distance_op\": \"<=>\",\n", + " \"index_metric\": \"cosine\",\n", + " \"description\": \"GloVe-100 Angular (1.18M train, 10k test, dim=100)\",\n", + "}\n", + "\n", + "\n", + "# ==========================================\n", + "# 5. CORE HELPER FUNCTIONS\n", + "# ==========================================\n", + "def download_dataset(dataset_cfg):\n", + " file_name = dataset_cfg[\"file_name\"]\n", + " if not os.path.exists(file_name):\n", + " url = dataset_cfg[\"url\"]\n", + " print_step(\n", + " \"📥\",\n", + " f\"Downloading {dataset_cfg['description']} from {url}...\",\n", + " )\n", + " req = urllib.request.Request(url, headers={\"User-Agent\": \"Mozilla/5.0\"})\n", + " with (\n", + " urllib.request.urlopen(req, timeout=60) as response,\n", + " open(file_name, \"wb\") as out_file,\n", + " ):\n", + " shutil.copyfileobj(response, out_file)\n", + " print_step(\"✅\", f\"Dataset {file_name} downloaded successfully.\", is_success=True)\n", + " else:\n", + " print_step(\n", + " \"ℹ️\",\n", + " f\"Dataset {file_name} already exists locally. Skipping download.\",\n", + " )\n", + "\n", + "\n", + "def setup_schema(cur, dataset_cfg):\n", + " dim = dataset_cfg[\"dim\"]\n", + " tbl = dataset_cfg[\"table_name\"]\n", + " tbl_test = dataset_cfg[\"test_table_name\"]\n", + "\n", + " cur.execute(\"CREATE EXTENSION IF NOT EXISTS vector CASCADE;\")\n", + " cur.execute(\"CREATE EXTENSION IF NOT EXISTS alloydb_scann CASCADE;\")\n", + " cur.execute(f\"DROP TABLE IF EXISTS {tbl} CASCADE;\")\n", + " cur.execute(f\"DROP TABLE IF EXISTS {tbl_test} CASCADE;\")\n", + " cur.execute(\n", + " f\"CREATE TABLE {tbl} (id bigserial PRIMARY KEY, embedding vector({dim}));\"\n", + " )\n", + " cur.execute(\n", + " f\"CREATE TABLE {tbl_test} (id bigserial PRIMARY KEY, embedding\"\n", + " f\" vector({dim}));\"\n", + " )\n", + "\n", + "\n", + "def stream_bulk_insert(\n", + " cur,\n", + " table_name,\n", + " column_name,\n", + " numpy_matrix,\n", + " batch_size=5000,\n", + " desc=\"Inserting\",\n", + "):\n", + " total_rows = len(numpy_matrix)\n", + " for i in tqdm(\n", + " range(0, total_rows, batch_size),\n", + " desc=desc,\n", + " leave=True,\n", + " colour=\"#1f77b4\",\n", + " ):\n", + " batch = numpy_matrix[i : i + batch_size]\n", + " placeholders = \", \".join([\"(%s)\"] * len(batch))\n", + " params = [str(vec.tolist()) for vec in batch]\n", + " cur.execute(\n", + " f\"INSERT INTO {table_name} ({column_name}) VALUES {placeholders}\",\n", + " params,\n", + " )\n", + "\n", + "\n", + "def ingest_dataset(cur, dataset_cfg):\n", + " file_name = dataset_cfg[\"file_name\"]\n", + " tbl = dataset_cfg[\"table_name\"]\n", + " tbl_test = dataset_cfg[\"test_table_name\"]\n", + "\n", + " with h5py.File(file_name, \"r\") as f:\n", + " train_data = f[\"train\"][:]\n", + " test_data = f[\"test\"][:]\n", + "\n", + " stream_bulk_insert(\n", + " cur,\n", + " tbl,\n", + " \"embedding\",\n", + " train_data,\n", + " batch_size=5000,\n", + " desc=f\"Inserting {len(train_data):,} Training Records\",\n", + " )\n", + " stream_bulk_insert(\n", + " cur,\n", + " tbl_test,\n", + " \"embedding\",\n", + " test_data,\n", + " batch_size=5000,\n", + " desc=f\"Inserting {len(test_data):,} Test Queries\",\n", + " )\n", + "\n", + "\n", + "def build_scann_index(cur, dataset_cfg, index_name=\"alloydb_scann_idx\"):\n", + " tbl = dataset_cfg[\"table_name\"]\n", + " metric = dataset_cfg[\"index_metric\"]\n", + "\n", + " # VACUUM ANALYZE to ensure accurate statistics for ScaNN AUTO mode\n", + " cur.execute(f\"VACUUM (DISABLE_PAGE_SKIPPING, ANALYZE) {tbl};\")\n", + " cur.execute(\n", + " f\"VACUUM (DISABLE_PAGE_SKIPPING, ANALYZE) {dataset_cfg['test_table_name']};\"\n", + " )\n", + "\n", + " cur.execute(f\"DROP INDEX IF EXISTS {index_name};\")\n", + " cur.execute(\n", + " \"SELECT set_config('max_parallel_maintenance_workers',\"\n", + " \" current_setting('max_parallel_workers'), false);\"\n", + " )\n", + "\n", + " # Scale maintenance_work_mem to at least 10% of table size (minimum 2GB for fast in-memory sampling)\n", + " cur.execute(f\"SELECT pg_catalog.pg_relation_size('{tbl}');\")\n", + " table_size_bytes = cur.fetchone()[0]\n", + " maint_mem_mb = max(2048, int((table_size_bytes * 0.10) / (1024 * 1024)))\n", + " cur.execute(f\"SET maintenance_work_mem = '{maint_mem_mb}MB';\")\n", + "\n", + " t0 = time.time()\n", + " cur.execute(\n", + " f\"CREATE INDEX {index_name} ON {tbl} USING scann (embedding {metric}) WITH\"\n", + " \" (mode = 'AUTO');\"\n", + " )\n", + " build_duration_sec = time.time() - t0\n", + "\n", + " cur.execute(\n", + " f\"SELECT pg_catalog.pg_size_pretty(pg_catalog.pg_relation_size('{index_name}'));\"\n", + " )\n", + " index_size = cur.fetchone()[0]\n", + " cur.execute(\n", + " f\"SELECT pg_catalog.pg_size_pretty(pg_catalog.pg_relation_size('{tbl}'));\"\n", + " )\n", + " table_size = cur.fetchone()[0]\n", + "\n", + " print_step(\n", + " \"✅\",\n", + " f\"ScaNN index '{index_name}' built in {build_duration_sec:.2f}s | \"\n", + " f\"Index Size: {index_size} (Table Size: {table_size})\",\n", + " is_success=True,\n", + " )\n", + "\n", + "\n", + "def deploy_benchmark_functions(cur, dataset_cfg):\n", + " tbl = dataset_cfg[\"table_name\"]\n", + " tbl_test = dataset_cfg[\"test_table_name\"]\n", + " op = dataset_cfg[\"distance_op\"]\n", + "\n", + " # 1. Recall measurement function (using evaluate_query_recall and scann.pct_leaves_to_search)\n", + " cur.execute(f\"\"\"\n", + " CREATE OR REPLACE FUNCTION measure_recall(pct_leaves_to_search FLOAT, num_q INT, k INT)\n", + " RETURNS FLOAT AS $func$\n", + " DECLARE\n", + " q_vec text;\n", + " query_str text;\n", + " total_recall FLOAT := 0.0;\n", + " valid_queries INT := 0;\n", + " v_id int; v_q text; v_p json; v_recall float; v_at float; v_et float; v_idx text;\n", + " BEGIN\n", + " IF num_q <= 0 THEN RETURN 0.0; END IF;\n", + "\n", + " FOR q_vec IN SELECT embedding::text FROM {tbl_test} LIMIT num_q LOOP\n", + " query_str := pg_catalog.format('SELECT id FROM {tbl} ORDER BY embedding {op} ''%s'' LIMIT %s', q_vec, k);\n", + " BEGIN\n", + " EXECUTE pg_catalog.format('SELECT * FROM evaluate_query_recall($$%s$$, ''{{\"scann.pct_leaves_to_search\": %s, \"scann.num_leaves_to_search\": 0}}'', ''{{\"scann\"}}'')', query_str, pct_leaves_to_search)\n", + " INTO v_id, v_q, v_p, v_recall, v_at, v_et, v_idx;\n", + " IF v_recall IS NOT NULL THEN\n", + " total_recall := total_recall + v_recall;\n", + " valid_queries := valid_queries + 1;\n", + " END IF;\n", + " EXCEPTION\n", + " WHEN OTHERS THEN\n", + " RAISE WARNING 'Error during evaluate_query_recall: %', SQLERRM;\n", + " END;\n", + " END LOOP;\n", + "\n", + " IF valid_queries = 0 THEN RETURN 0.0; END IF;\n", + " RETURN total_recall / valid_queries;\n", + " END;\n", + " $func$ LANGUAGE plpgsql;\n", + " \"\"\")\n", + "\n", + " # 2. QPS measurement function (using scann.pct_leaves_to_search)\n", + " cur.execute(f\"\"\"\n", + " CREATE OR REPLACE FUNCTION measure_qps(pct_leaves_to_search FLOAT, num_q INT, k INT)\n", + " RETURNS FLOAT AS $func$\n", + " DECLARE\n", + " q_vec vector;\n", + " start_time timestamp;\n", + " end_time timestamp;\n", + " total_time_ms float;\n", + " BEGIN\n", + " IF num_q <= 0 THEN RETURN 0.0; END IF;\n", + "\n", + " PERFORM pg_catalog.set_config('scann.num_leaves_to_search', '0', false);\n", + " PERFORM pg_catalog.set_config('scann.pct_leaves_to_search', pct_leaves_to_search::text, false);\n", + "\n", + " start_time := pg_catalog.clock_timestamp();\n", + " FOR q_vec IN SELECT embedding FROM {tbl_test} LIMIT num_q LOOP\n", + " EXECUTE pg_catalog.format('SELECT id FROM {tbl} ORDER BY embedding {op} $1 LIMIT %s', k) USING q_vec;\n", + " END LOOP;\n", + " end_time := pg_catalog.clock_timestamp();\n", + "\n", + " total_time_ms := (EXTRACT(epoch FROM end_time) - EXTRACT(epoch FROM start_time)) * 1000.0;\n", + " IF total_time_ms <= 0.0 THEN RETURN 0.0; END IF;\n", + "\n", + " RETURN (num_q * 1000.0) / total_time_ms;\n", + " END;\n", + " $func$ LANGUAGE plpgsql;\n", + " \"\"\")\n", + "\n", + "\n", + "def run_benchmark_sweep(cur, pct_values, num_queries, top_k):\n", + " print_step(\"🔥\", \"Warming up database cache (0.5% leaves to search)...\")\n", + " # Configure query execution memory and parallelism once for the benchmark session\n", + " cur.execute(\"SET work_mem = '256MB';\")\n", + "\n", + " cur.execute(\n", + " \"SELECT measure_qps(0.5, %s, %s);\",\n", + " (min(50, num_queries), top_k),\n", + " )\n", + "\n", + " results = []\n", + " for pct in tqdm(\n", + " pct_values,\n", + " desc=\"Benchmarking ScaNN Auto Search (% Leaves)\",\n", + " colour=\"#1f77b4\",\n", + " ):\n", + " cur.execute(\n", + " \"SELECT measure_recall(%s, %s, %s);\",\n", + " (pct, num_queries, top_k),\n", + " )\n", + " recall = cur.fetchone()[0]\n", + " cur.execute(\n", + " \"SELECT measure_qps(%s, %s, %s);\",\n", + " (pct, num_queries, top_k),\n", + " )\n", + " qps = cur.fetchone()[0]\n", + " results.append({\"pct\": pct, \"recall\": recall, \"qps\": qps})\n", + " return results\n", + "\n", + "\n", + "def display_summary_table(results, top_k):\n", + " print_step(\"📊\", \"Benchmark Summary Table:\", is_success=True)\n", + " rows_html = \"\".join([\n", + " f\"\"\"\n", + " {r['pct']}%\n", + " = 0.9 else 'inherit'}; font-weight: {'bold' if r['recall'] >= 0.9 else 'normal'};\">{r['recall']:.4f}\n", + " {r['qps']:.1f}\n", + " \"\"\"\n", + " for r in results\n", + " ])\n", + " table_html = f\"\"\"\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " {rows_html}\n", + " \n", + "
pct_leaves_to_searchRecall @ {top_k}QPS (Queries/sec)
\n", + " \"\"\"\n", + " display(HTML(table_html))\n", + "\n", + "\n", + "def plot_results(results, dataset_cfg, top_k):\n", + " recalls = [r[\"recall\"] for r in results]\n", + " qps_vals = [r[\"qps\"] for r in results]\n", + " pct_labels = [r[\"pct\"] for r in results]\n", + "\n", + " plt.rc(\"font\", size=14)\n", + " plt.rc(\"axes\", titlesize=16)\n", + " plt.rc(\"axes\", labelsize=14)\n", + " plt.rc(\"xtick\", labelsize=12)\n", + " plt.rc(\"ytick\", labelsize=12)\n", + " plt.rc(\"legend\", fontsize=13)\n", + "\n", + " fig, ax = plt.subplots(figsize=(12, 7), dpi=120)\n", + "\n", + " color_scann = \"#1f77b4\"\n", + "\n", + " ax.plot(\n", + " recalls,\n", + " qps_vals,\n", + " marker=\"o\",\n", + " linewidth=2.5,\n", + " markersize=9,\n", + " color=color_scann,\n", + " label=\"AlloyDB ScaNN (Auto Mode)\",\n", + " )\n", + " ax.fill_between(recalls, qps_vals, color=color_scann, alpha=0.08)\n", + "\n", + " ax.set_xlabel(f\"Recall @ {top_k}\", fontweight=\"bold\", labelpad=10)\n", + " ax.set_ylabel(\"Queries Per Second (QPS)\", fontweight=\"bold\", labelpad=10)\n", + " ax.set_title(\n", + " f\"AlloyDB ScaNN: Recall vs QPS\\n({dataset_cfg['description']}, mode=AUTO,\"\n", + " f\" K={top_k})\",\n", + " fontsize=15,\n", + " fontweight=\"bold\",\n", + " pad=20,\n", + " )\n", + "\n", + " max_qps = max(qps_vals) if qps_vals else 100\n", + " min_recall = min(recalls) if recalls else 0.5\n", + " ax.set_ylim(0, max_qps * 1.18)\n", + " ax.set_xlim(max(0.0, min_recall - 0.05), 1.02)\n", + "\n", + " ax.yaxis.set_major_locator(ticker.MaxNLocator(8))\n", + " ax.xaxis.set_major_locator(ticker.MultipleLocator(0.05))\n", + "\n", + " # Clean spines for compatibility across light and dark themes\n", + " ax.spines[\"top\"].set_visible(False)\n", + " ax.spines[\"right\"].set_visible(False)\n", + " ax.spines[\"left\"].set_color(\"#cccccc\")\n", + " ax.spines[\"bottom\"].set_color(\"#cccccc\")\n", + "\n", + " plt.grid(True, which=\"both\", color=\"#f0f0f0\", linestyle=\"-\", linewidth=1.5)\n", + "\n", + " pe = [patheffects.withStroke(linewidth=3, foreground=\"white\", alpha=0.9)]\n", + "\n", + " for i in range(len(recalls)):\n", + " ax.text(\n", + " recalls[i],\n", + " qps_vals[i] + (max_qps * 0.03),\n", + " f\"PCT={pct_labels[i]}%\\n({qps_vals[i]:.0f} QPS)\",\n", + " color=\"#D97706\",\n", + " fontsize=9,\n", + " fontweight=\"bold\",\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " path_effects=pe,\n", + " )\n", + "\n", + " ax.legend(loc=\"lower left\", frameon=True, edgecolor=\"#cccccc\")\n", + " fig.tight_layout()\n", + " plt.show()\n", + "\n", + "\n", + "# ==========================================\n", + "# 6. MAIN EXECUTION PIPELINE\n", + "# ==========================================\n", + "connector = None\n", + "conn = None\n", + "cur = None\n", + "\n", + "try:\n", + " print_step(\"✅\", \"GCP Authentication & Setup Successful!\", is_success=True)\n", + " print_step(\n", + " \"🚀\",\n", + " \"Starting AlloyDB ScaNN Benchmark Pipeline (Auto Mode - % Leaves\"\n", + " \" Search)...\",\n", + " )\n", + "\n", + " dataset_cfg = DATASET_CONFIG\n", + "\n", + " # Step 1: Connect to AlloyDB\n", + " print_step(\n", + " \"Step 1/7:\",\n", + " \"🔌 Initializing Secure AlloyDB Connector...\",\n", + " )\n", + " connector = Connector()\n", + " conn = connector.connect(\n", + " f\"projects/{project_id}/locations/{region}/clusters/{cluster_id}/instances/{instance_id}\",\n", + " \"pg8000\",\n", + " user=db_user,\n", + " password=db_pass,\n", + " db=db_name,\n", + " ip_type=\"PUBLIC\",\n", + " )\n", + " conn.autocommit = True\n", + " cur = conn.cursor()\n", + "\n", + " # Step 2: Database Schema & Extensions\n", + " print_step(\n", + " \"Step 2/7:\",\n", + " \"🛠️ Setting up pgvector & alloydb_scann extensions...\",\n", + " )\n", + " setup_schema(cur, dataset_cfg)\n", + "\n", + " # Step 3: Download Dataset\n", + " print_step(\n", + " \"Step 3/7:\",\n", + " f\"📥 Checking & Downloading {dataset_cfg['description']}...\",\n", + " )\n", + " download_dataset(dataset_cfg)\n", + "\n", + " # Step 4: Stream Ingestion\n", + " print_step(\n", + " \"Step 4/7:\",\n", + " f\"📂 Ingesting training data & test queries into AlloyDB ({dataset_cfg['table_name']})...\",\n", + " )\n", + " ingest_dataset(cur, dataset_cfg)\n", + "\n", + " # Step 5: Build ScaNN Index in AUTO Mode\n", + " print_step(\n", + " \"Step 5/7:\",\n", + " \"🏗️ Building ScaNN Index with mode='AUTO' (VACUUM ANALYZE &\"\n", + " \" auto-tuning)...\",\n", + " )\n", + " build_scann_index(cur, dataset_cfg)\n", + "\n", + " # Step 6: Deploy Benchmark Functions\n", + " print_step(\n", + " \"Step 6/7:\",\n", + " \"⚙️ Deploying Benchmark Functions (measure_recall & measure_qps)...\",\n", + " )\n", + " deploy_benchmark_functions(cur, dataset_cfg)\n", + "\n", + " # Step 7: Sweep & Evaluation\n", + " print_step(\n", + " \"Step 7/7:\",\n", + " \"⏱️ Benchmarking ScaNN recall & QPS across search partition\"\n", + " \" percentages...\",\n", + " )\n", + " pct_values = [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]\n", + " benchmark_results = run_benchmark_sweep(cur, pct_values, num_queries, top_k)\n", + "\n", + " # Display Table & Plot\n", + " display_summary_table(benchmark_results, top_k)\n", + " plot_results(benchmark_results, dataset_cfg, top_k)\n", + "\n", + "except Exception as e:\n", + " print_step(\"❌\", f\"ERROR: {str(e)}\", is_error=True)\n", + "\n", + "finally:\n", + " if \"cur\" in locals() and cur:\n", + " cur.close()\n", + " if \"conn\" in locals() and conn:\n", + " conn.close()\n", + " if \"connector\" in locals() and connector:\n", + " connector.close()\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "bb2bb0d3c235439a8dd8b8cb9f544191", + "f0d4e9e968724c098b56d8733e8cb42a", + "9b904fc7fefd4ed2b7282dfff26e8244", + "631658ca63714d60a1cfadf61adde2f3", + "78d279973dad446b9b85635c724c0455", + "a224f47dfa8d40cfaafd52c30741db33", + "eb975b210f304ea6a2d9ae8081e46a75", + "e510a1e31be54e3b82832f1047768ad1", + "1a928fcb6f964fdca0fe3d08b5141224", + "0cde9f71448f40ba9f33d63d97f25bbf", + "5038b51e1d454b5e85f92bc905c9e5c3", + "0f1b3c48bca840a9ade73b114aa97033", + "bd95243e64284400ae94adda764dd676", + "4211e3547ca54b80ab8bd83cd18205ed", + "722cad3215fe4f8aac048fe76301f882", + "e1d193b7e60f4914a5477d618b1d2ec5", + "0a6cad141bb543ff85abe1752e92f1f0", + "42ece952d02b44c49a521f7181d6d039", + "7cae70365604497db1f33470ca3e329f", + "1f96e9c7b40e41f4882af89dae656820", + "4510043017864ff8ad0e38e33494b5fe", + "6f0d2aada81f42e3b645ba0c78f6f2dc", + "b7489eb0b544467794893578b208fc30", + "2009e650f2e544d2bd1a87bdb80f4f17", + "4b014493a650429295b29f5e860c5cf8", + "25d9691c62b745148f36c3923f457e00", + "1dd50d6fd07449ddbe838f17bf04d557", + "452d933c977342b9b7809a62bcb7114d", + "66b1ecc6a2ee4e9ab9186b4af0ee5fa0", + "dd57d3e0dfa84249a220d1472a88dfdc", + "79c81933da0c4dcfa8a2130f77c1a281", + "e956f439a7ef4b16a5a01b6c3968920b", + "32f09bc5af3e4bfebd4abd146656b47a" + ] + }, + "cellView": "form", + "outputId": "6ef00517-3025-46cb-be64-dff5b25530ea", + "id": "9WxWMjxAwH1e" + }, + "execution_count": 1, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " ✅ GCP Authentication & Setup Successful!\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " 🚀\n", + " Starting AlloyDB ScaNN Benchmark Pipeline (Auto Mode - % Leaves Search)...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 1/7:\n", + " 🔌 Initializing Secure AlloyDB Connector...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 2/7:\n", + " 🛠️ Setting up pgvector & alloydb_scann extensions...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 3/7:\n", + " 📥 Checking & Downloading GloVe-100 Angular (1.18M train, 10k test, dim=100)...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " 📥\n", + " Downloading GloVe-100 Angular (1.18M train, 10k test, dim=100) from https://ann-benchmarks.com/glove-100-angular.hdf5...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " ✅ Dataset glove-100-angular.hdf5 downloaded successfully.\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 4/7:\n", + " 📂 Ingesting training data & test queries into AlloyDB (glove)...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Inserting 1,183,514 Training Records: 0%| | 0/237 [00:00" + ], + "text/html": [ + "\n", + "
\n", + " Step 5/7:\n", + " 🏗️ Building ScaNN Index with mode='AUTO' (VACUUM ANALYZE & auto-tuning)...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " ✅ ScaNN index 'alloydb_scann_idx' built in 90.53s | Index Size: 287 MB (Table Size: 519 MB)\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 6/7:\n", + " ⚙️ Deploying Benchmark Functions (measure_recall & measure_qps)...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 7/7:\n", + " ⏱️ Benchmarking ScaNN recall & QPS across search partition percentages...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " 🔥\n", + " Warming up database cache (0.5% leaves to search)...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Benchmarking ScaNN Auto Search (% Leaves): 0%| | 0/7 [00:00" + ], + "text/html": [ + "\n", + "
\n", + " 📊 Benchmark Summary Table:\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
pct_leaves_to_searchRecall @ 10QPS (Queries/sec)
0.1%0.65502134.7
0.25%0.77701671.2
0.5%0.84601293.4
1.0%0.8890902.9
2.0%0.9230576.1
5.0%0.9590279.2
10.0%0.9740150.6
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAABYUAAAMtCAYAAADAKh5gAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzs3Xd8E+UfB/BPRvfeQBllQ8veQzZUhiwZIqBsFQFBUdwCgj8cICCoKCAgIDJkFQRktGXvWVpaRsssdO+d3O8PbOzlkjZp07TQz/v1ygty9zzPPZc8uVy/ee57MkEQBBARERERERERERFRhSAv6w4QERERERERERERkfkwKExERERERERERERUgTAoTERERERERERERFSBMChMREREREREREREVIEwKExERERERERERERUgTAoTERERERERERERFSBMChMREREREREREREVIEwKExERERERERERERUgTAoTERERERERERERFSBMChMREREREREREREVIEwKExERERERERERERUgTAoTERERM+lOXPmQCaTaR4+Pj6SMj4+PqIyc+bMMXs/iejZUfB4IZPJsHbtWtH6tWvXSsoQERERlUcMChMREVG5tXPnTkmARSaTYejQoWXdNbMaO3as5DWQy+WwsrKCk5MTqlevjnbt2mHMmDFYuXIlUlNTi2xT1+ua/7CwsICzszP8/PwwYsQIrF+/Hrm5uSXej4yMDPzyyy/o27cvqlatChsbG9jY2MDb2xuNGzdG37598fHHH2Pz5s2IiYkp8fZMTVfATy6X49KlSzrLa/8woStAqKuMra0toqOjdbapPRZ0/dhhSoaMk8aNG+P1119HQEBAqfaFyqfk5GQsX74cL7/8MmrVqgUnJydYWlrCy8sLLVu2xDvvvIMjR44U2Y72j3TaDxsbG1SpUgU9e/bEd999h9jY2ELbu3nzJmbNmoW2bdvCzc0NFhYWcHJyQq1atdC6dWuMHj0a3333HQ4ePAi1Wm2ql4OIiOiZoSzrDhARERHpoz0LL19AQAASEhLg6upq3g6VI4IgICcnBzk5OUhJScH9+/dx5swZ/P7773jvvfcwbdo0zJ07FxYWFka3nZeXh+TkZCQnJyM0NBSbN2/GggULsH//flSvXr1Y/Q0JCcGAAQMQGRkpWffo0SM8evQIISEh2LdvHwBg2bJlmDp1arG2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2c68efNEZRs3biwZT7o+K8uWLdOs/+qrryTbXLp0qc7t5eTk6E1hoc/UqVNFbffu3bvI18PU6SMEQRCSkpIEGxsbURtnz57VrA8LCxOtUyqVomPk5MmTRev//PNPvdvSzrtclCdPnkj2r3379kXW279/v6Te22+/LSqj/d4b+tixY4eonVu3bknSVeh72Nvbaz6vREREFQ1nChMREVG5op06olevXqL0A9opJAICAkpltteDBw8klymPGzdOcmny8OHD4eLiIlp29OhRzf8VCoUoHQAA0QzEzZs3i9aNGDFC56X3xhAKXFaerzgzfPOpVCpMmjQJI0aMKFa6DplMhs2bN+O7776Dh4dHkeV3796N3r17i7Z17tw5pKWlicp98MEHktc+X/7l5AU9fvwYX331FXr06IFq1arBzs4OcrkcMpkM7u7ukvKGXmo+cuRING3aVPN8wYIFSElJMaiuPjNmzBCN+88++wwqlcqguj4+PhCe3jtE89A1e9yUXF1d8ffff2PatGk612vPXr127RoUCgVkMpnmoVQqcffuXVG5gp8l7XQDVapUkcwczmdhYaHzc7R3716MHTsWTZs2hYuLCywsLDTbX758uahsWaUacHJywssvvyxaVvDqCe0rKfr27SsaKy1bthStnzhxIl555RV8+eWX+PPPP3H16lXNWHJwcDB19w1m6HjWR6lU4vvvv5fMMK9duzbOnz+PV155RXRliS5paWl4//338f3335eoL0RERM8iBoWJiIio3IiNjcXff/8tWpafOiLfyy+/LAr25OTk6My5WVKPHj2SLKtdu7ZkmVwuR40aNQqtO2nSJFGfDx06hMjISACmTx0BAA8fPpQsKyoY26VLF00AMTc3F+Hh4RgxYoSozNatW4t9Sb1cLsf777+Phw8fYv/+/fjoo4/QtWtX2Nvb6yx/6dIl7Ny5U/Nc1/uhHfwqTEBAAOrUqYPPPvsMR44cwYMHD5CRkaEzgJ5POwitj0wmw//+9z/N8/j4eHz77bcG900XOzs7fP7555rnN27cwJo1a0rUZmlKSEiAv7+/zlQHgO4xaYjo6GjN/7XHQLNmzYoM+uXLyMhA79698dJLL2HdunW4evUqkpKSCv2Rw9D3vzRoHwc2b94MtVoNoPDUEcDTH87atGmjeZ6WloYtW7Zg9uzZePXVVzUB8ddffx03b940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UMbT01NnOe3lBevpCuzoakfXsuIGhbTzqrm5uaFPnz6wsLCQnJgZk2NNEAS0bNkSBw8exOXLlyVBOrVajc2bN4uWrVq1SnJS1b59ewQGBiIsLAxLly6FlZVVuTpJt7W1Rb9+/fDjjz9i//79uHDhgibX9y+//CIJHBb2B3++vLw8WFhYYPbs2bh48SKuXr2K33//HfXq1TO4X4Yeb55V//vf//DGG29o8rM9fPgQXbp0ga2tLWrUqIGtW7dqytatWxf79++XHL908fPzQ/fu3TXPly9fDgDYunWr6Metd955p8T78OeffyIyMhJDhgwRLdeVO7Bdu3Z628nLy0OlSpWwcuVKhIWF4dy5c1i4cCHs7e0BPP1Dq3v37vj++++xd+9enD17Fjdv3sTFixfx+++/S/5w+e6770q8b0DxjgHlSWkfj+/cuYNu3brh0aNHmmVt27bFkSNH4O7ubnA/hw4disjISJ2BzmPHjonGUf7xRxAETJgwAenp6Zqy9evXx8aNGxESEoLz58/j448/FuU//O2333DkyBHN86NHj4q21aJFCxw4cADh4eG4fv06/vnnH3z33Xfo27cvrK2tNeXyb1qonXvR29tbMu61c86WVGxsrOTcwtbWFj/++COuXLmCXbt2oUGDBnrvhVBSgiDgww8/REhICPbs2SN5n48cOQKlUolFixYhNDQUa9asEeWVBCD5Mb68MtXnxxTnVgVZWlqiQYMGmueCIBj8I44pCIKAcePG4fLlywgKCkLt2rVF64OCgiAIAj766COEhIRgx44dknzV2mPgwYMHeP/990XLnJ2d8csvv+DKlSvYu3cvWrZsKVq/f/9+0c2ec3Nz8fHHH4vKKJVKzJ8/HxcvXsSBAwfQoUOHIj8bmZmZmDBhgujvmLZt22LHjh0ICwvDqVOnJDcUnDt3rtl+INR1D438G/C++uqror+1nqX7aVy/fl2yrHXr1oXW0bVe300aTe3+/fuivwOtrKywfft2DBo0yCzbN7V79+5Jfsgp6flC06ZNRc+vXr2q98Z0VMGVzQRlIioO7UuF9eV52rJli+TSklmzZknKFXX5jPblgEVd6lirVi3R+po1a0ouF920aZNkOz/99JOkb9q5upo0aSJar33JUqVKlUR55iZMmCBa369fP8k20tLSBGtra1E5U6aRMDZ9hPYle506ddJZ7oUX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C6hPc5566inuvffeBufS09N566238Hq9SgpLi7U1VmwGgPnTl8iB1b/l9B2klUjgFGsSLIo1CZa2xJrNAjeNT2V41yj+8MFmCqtc/ms+E55alMOKnDIemNaHxEhHu85Xjm56HynBpHiTYFGsNU9J4SZ4vV4uu+wyvv32W2bOnMlvfvObwz2lBn7xi19wwQUXNDjn8/lwuVxYrVasVuthmpkcDfb/D2JbYsXrM/H4fFgMQyuF5SB8GOz904GqFcmhpFiTYFGsSbAEHmsnpMUz++ph/PHDLL7bXtrg2oqcci7832oeOKcvY3vEBzpZOYoF+t5ApDUUbxIsirWWUVL4Z7xeL7NmzeLtt9/mwgsv5NVXX220Wd3eVb17V//+3N7zsbGxAfVpTlJSEklJSQ3O1dbWkpmZSXh4OGFhYQe9hxy/At2Fs9rpoazKSUSIDUsLNnKU41f9x1190IKd00UCoViTYFGsSbC0V6zFxoTw+KXDeWlJNk/N34bX3Pcpr5IaNze+uZ5fnNyD20/tjc2qP3QcjwJ9byDSGoo3CRbFWssE/Jv/6quv9n+tX7++PeZ02Ljdbi6++GLefPNNLrnkEl5//fUm6/9GRETQpUsXqqqq2LVrV6PrW7ZsAaB3797+cz169MBqtfLjjz/i8Xha1EdERERERCQQFsPgqrHdefaKESRHN0wwmyY8OX8bF/13KflltYdphiIiInI4BJwUfvnll3nppZd4+eWXSU9Pb485HRYul4sLLriAt99+m8svv5xXXnnlgEvMJ02aBMCnn37a6Nonn3zSoA1AaGgoY8aMoaamhsWLF7eoj4iIiIiISHsYkhrLG9eNZkKvDo2urcwpZcpji/lq457DMDMRERE5HAJOCiclJWGaJjExMURERLTHnILO6XQyffp03n//fa655hpeeOEFLJYD/2huvPFGAP72t79RWrqvRld2djZPPvkkISEhXHXVVQ363HTTTQDcfffd1NXV+c+vWLGC2bNnk5iYyIwZM9rrsURERERERPxiwx38a+YQ7pjcG5ulYRmwsho317y0kvs/ysTl0UaKIiIix7qAawqffPLJvPnmm1RUVFBeXu6vnXu4zZ07l7lz5wKwe/duAL777juuvPJKADp06MD//d//AfUJ3nnz5tGhQwe6dOnCX//610b3O/nkkzn55JP9348ZM4Y77riDRx55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clL4l19+4dZbb+Wwww6jZ8+eDBkyBIDx48czbty4hAPnRERERERERPYl7Vuk8NqlB3Hu4Hb1HmsDoYjF70fPYNaqskaPTUREZFsNrikMEIlEuOGGG3j++efj92zbplWrVgDcfPPNzJw5E6fTyerVq8nNzW2MaUVERERERET2KG6nyR0jezKwQxZ3fvgrVj0KNm5JDP/1y0Lev+aQ3RajiIhIo+wUvuqqq3j++efZUp74t2WKR40ahW3bRCIRPv3008aYUkRERERERGSP1S47uV4J4S1sYNryUuavr2j0mERERLZocFJ47NixvPbaawAYhoFt2xi/OXzq6KOPTugvIiIiIiIisi/7cMbqBo1/a8rKRopERESkpgYnhV944YX4dVZWFg8++GCNncK9evWKJ4pnz57d0ClFRERERERE9mgL1ldi7LxbrQygcJ12CouIyO7T4KTw5MmT49cff/wxd999N0DCbmGv10taWhq2bbNq1aqGTikiIiIiIiKyR6sKRdmF6hFArISEPxhpzHBEREQSNDgpvHHjRgBSUlI47LDDtj+RGZuqqqqqoVOKiIiIiIiI7NGS3Y4G7RRO8TTKufAiIiK1anBS2O12AxAKhWqUjdiisrKSsrIyIJY8FhEREREREdmX9WiV1qCdwge0SmvMcERERBI0OCncpk0bAMLhMB988EGtfd566y0gVlKiffv2DZ1SREREREREZI925oC2DRq/ZJOPyupwI0UjIiKSqMFJ4WHDhgFg2zaXXnop999/f7wtGAzy17/+lZtvvrlGfxEREREREZF9VfeWafRrl7nL4ycvLeHUpyayaENl4wUlIiKyWYOTwpdffjkQ2wVcVVXFAw88AMSSxGVlZdx7770Eg8F4/8suu6yhU4qIiIiIiIjs8W45pjtup7nLtYWXFvk59emJfPnrukaNS0REpMFJ4SFDhnDxxRdj2zaGYcR/N4zYX3vbfn3ppZcyaNCgBgctIiIiIiIisqc7sE0Gfx/VB5ejbh+9XaZBituRcK8qFOXat2fw0FfziESt3RGmiIjshxqcFAZ44YUXOP/88+MHzdX2+3nnncezzz7bGNOJiIiIiIiI7BUO65bLSxcPov82pSSM3/wO0L9dJi9fMpj3rh7KAfk1D5l7YdxSLnx5KkW+YI02ERGR+jLsLZnbRjB+/HhGjx7NzJkzKSsrIzMzk379+nH++edz+OGHN9Y0UotAIEBhYSEFBQV4vd7mDkf2YD6fj2g0isPhIDU1td7j/cEIRb4gKR4nprGrPwgn+wMzEsTAwsbEcnqaOxzZh2mtSVPRWpOmorW271q0oZIPZqxm4fpK/KEoKW4H3VulMWpAW7q13JoIDkaiPPz1fD6dtbbGM/Izknjm/AH0b5/V4Hga+tlApD603qSpaK3VTaMmhaX5KCksdaWksDQVfaCVpqK1Jk1Fa02aitaaQOwnbj+auYZHx8wnHE382O52mNx3Si/OO6hdvHTjrlDiRJqS1ps0Fa21ummU8hEiIiIiIiIi0ngMw+DMAW156eLB5KUlfnMgFLW4++PZ3Pnhr1SHo80UoYiI7M2c9em8cuVKABwOB23atEm4Vx/t27ev9xgRERERERGR/U3vNhm8feUQ/vjhr/y8ojSh7T8/r2b++kqeOX8AbbOSmylCERHZG9UrKdyxY0cMw6BVq1asWbMm4V5dGYZBJBKpX5QiIiIiIiIi+6nsFDfPXDCAJ8cu5s3JKxLafl1dzslPTuDJ8wZwaLecZopQRET2NvUuH2HbNr8tQ7zlXl1/iYiIiIiIiEjdOU2Tm4/uzsNn9MbrciS0lVaFueiVn3jmh8X6zC0iInVS76RwbbuCDcOo0y8RERERERER2XXH9mrFG5cdRIfsxHIRlg2PjlnANaOnU1kdbqboRERkb7FLO4Vru6cdwiIiIiIiIiK7X5e8VN64/CCO6J5bo+2buRs49emJLN5Y2QyRiYjI3qJeSWHLsrAsi7Vr19a4V9df0ahORhURERERERFpiLQkF4+d3Zdrj+zKb38ud+kmP6c+NZGvZ69rlthERGTPV++dwiIiIiIiIiLS/EzD4PJDO/Hk7/qT4XUltPlDUX7/1gz+9vU8IlGrmSIUEZE9VYOTwp07d47/mjp1amPEJCIiIiIiIiJ1dEiXHN664mB6tkqr0fb8j0u56JWpFPuCzRCZiIjsqRqcFF67di3Lly9nzZo1DBo0qDFiEhEREREREZF6aJ3p5ZVLBnNy39Y12iYtKebkJyfwy6qypg9MRET2SA1OCrdr1w6A1NRUTHPvr0bx5Zdfcuyxx9K2bVu8Xi+dO3fmrLPOYvLkybX2nzRpEieccALZ2dl4vV769OnDE088scPayV988QVHHHEEGRkZpKamcvDBB/P666/vrpckIiIiIiIi+4Ekl4P7Ti7g7hN64jQTKw2vLa/mrOcm8+7Ulc0UnYiI7EkanMU95ZRTACgrK2Pp0qUNDqg53XnnnZx00knMmDGDkSNHcuONNzJgwAA+/fRThg0bxujRoxP6f/rppwwfPpxx48Zx+umnc9111xEKhbj55ps599xza53jqaee4uSTT2bOnDlccMEFXHnllaxdu5ZLLrmE2267rSlepoiIiIiIiOyjDMNg1MB2vHTxIHLTPAltoajFHz+azV0f/UowokPgRUT2Z4Zt23ZDHlBaWkr//v1ZtWoVhx12GO+99x4tW7ZsrPiazPr162nTpg25ubn8+uuv5OXlxdu+//57RowYQadOneKJ74qKCrp27Up5eTkTJ06Ml86orq5mxIgRTJ48mXfeeSchObx8+XJ69uxJSkoK06dPp2PHjkDsPRw8eDBLlixh0qRJDB06tN7xBwIBCgsLKSgowOv1NuCdkH2dz+cjGo3icDhITU2t93h/MEKRL0iKx4lp/PacY5GtzEgQAwsbE8vp2fkAkV2ktSZNRWtNmorWmjSWYl+QP340m+krSmu09W2bwWOn9yQv1bXLnw1E6qOhn0VF6kprrW6cDX3Ak08+ydFHH80rr7zC+PHj6dy5MyNHjqRz586kpdUscg9w7733NnTaRrdixQosy+Lggw9OSAgDHHnkkaSlpbFp06b4vQ8++IBNmzZx0UUXJdRSTkpK4sEHH+Soo47i2WefTUgKv/LKKwSDQe688854QhggKyuLu+++m8svv5znnntul5LCIiIiIiIiIttqkerhmfMH8OTYRYyeklg24pfV5Zz98gweObUHh3Rp0UwRiohIc2lwUvi+++7D2Lxb0LZtAoEAn3zyyQ7H7IlJ4W7duuF2u5k6dSpFRUXk5OTE28aNG0dlZSWnnXZa/N7YsWMBGDlyZI1nDR8+nOTkZCZNmkQwGMTj8ex0zPHHH5/QR0RERERERKShXA6TW47pQa/8DO7/Yi7VYSveVloV5pp353DTiM5cf3TP+Gd7ERHZ9zW4fIRpmrX+xfHbxxqGgW3bGIaxw0PYmtMTTzzBLbfcQk5ODqeddhotWrRgyZIlfPbZZwwfPpzRo0fHdxEPHjyYn3/+mZ9//pmBAwfWeNaBBx7I3LlzKSws5IADDgAgNzeXoqIiioqKaNGi5ndiU1NT8fv9+P1+kpOTtxvnxo0bE3YtA1iWRSgUomPHjvEktEhttv3vz+Fw1H+8ZROxLEzDwED/aJQdsTCA2N8Ge/9BpLIn01qTpqK1Jk1Fa012j8Wb/Nz84VxWlVbXaDv2gBz+clIPUjwN3jsmUquGfhYVqav9ea3Vp1xGo/xpX5e8cgNzz03ipptuomPHjlx22WW8+OKL8ftdu3blkksuSSgrUV5eDkBGRkatz9pyv6ysrF5j/H4/5eXlO0wKP/PMM9x///0J9zp37sx//vMfotHoHpt0lz3Prq4VpwGxjyl7/n/X0vxi3zqwdtJLpOG01qSpaK1JU9Fak8bWLdfL25f0457PF/Lj4pKEtv/OK2LxRj//OPMAOrXY/udRkcagvIU0Fa217WtwUvjiiy9ujDj2CI8++ih33303N9xwA9dddx2tWrVi/vz53HXXXZx//vnMmjWLRx99tLnD5A9/+ANnnXVWwr0tO4UdDsd+910QqR/tFJamo11O0lS01qSpaK1JU9Fak90nLcnNP0f14uVJK3lm3IqEbR5LiwOc/9ov/PWUnhzdM2e7zxDZFfvz7k1pWlprddPg8hH7ih9++IEjjzyS008/nY8++iihraqqiu7du7Nu3ToWLVpE586dm7V8RG0CgQCFhYUUFBTg9XrrNVb2Lw09hdMfjFDkC5LicWKq5pjsgE5Ol6aitSZNRWtNmorWmjSVSQvWcfdn86mojtRo+/0RXbjt2B44TP2bXxpHQz+LitSV1lrd6NvOm33xxRcAHHnkkTXakpOTOeigg7Asi5kzZwLQo0cPABYuXFijfyQSYdmyZTidTjp37hy/v6Mx69atw+/307Zt23onhEVERERERETq69Au2bxzST96tEyp0fbsD0u45NWplPhDzRCZiIjsbrslKbxkyRKmTJnClClTdsfjd4tgMAhQ4wC3Lbbcd7vdAIwYMQKAMWPG1Og7btw4qqqqOOSQQxIOfdvRmK+//jqhj4iIiIiIiMju1jYridcu7MeJvfNrtI1fVMTJT05g9uryZohMRER2p0ZLCpeUlHDLLbeQm5tL9+7dGTZsGGeeeSYAv//977nsssu46aabGmu6RnfYYYcB8MILL7BmzZqEtq+//pqJEyeSlJTEIYccAsCoUaPIycnh3Xff5eeff473ra6u5p577gFir3tbl156KR6Ph6eeeorly5fH75eWlvLQQw8BcM011zT6axMRERERERHZHq/LwQOn9uLOkT1x/qZcxJqyAGc+N4n/TFvVTNGJiMju0OCD5gDmz5/PyJEjWbVqFduWKN5yXVJSwvvvv49hGFx44YW11uBtbqNGjeLoo4/mu+++44ADDuD000+nVatWzJs3jy+++ALbtnn44YfjtYDT09N58cUXGTVqFEcccQTnnnsu2dnZfPbZZyxYsIBRo0ZxzjnnJMzRqVMn/v73v3PDDTcwaNAgzjnnHNxuNx988AGrV6/m1ltvZejQoc3x8kVERERERGQ/ZhgG5wxuR89Wadz+wS8U+baWjQhFLO748FdmrS7jzycX4HHq4CYRkb1dg3cKV1dXc/LJJ7Ny5Ups28YwDIzfHD510kknxa+//PLLhk65W5imyVdffcU///lPCgoK+Pjjj3n88ceZMmUKJ5xwAt988w033nhjwpjTTjuNH3/8keHDh/Phhx/y5JNP4nK5+Mc//sG7775b430AuP766/nss8/o1asXb7zxBi+88AKtWrXitdde47HHHmuqlysiIiIiIiJSQ992mbx95RD6t8+s0fb2Tys55/kprCsPNH1gIiLSqAx72629u+Bf//oXN998M4ZhxHcGb7lu1aoVa9euZdmyZXTp0gXDMDj66KP55ptvGiV42SoQCFBYWEhBQQFer7e5w5E9WENP4fQHIxT5gqR4nJi1fONDZAudnC5NRWtNmorWmjQVrTVpKjtaa+GoxRPfLeKdqStrjGuR4uap3w1gaJcWuzSvFQkT9a3/zV0D05uFw1Pz0LuEuMrXEtpQiBWpxpXZHnfLAkyHk2h1BVb1zmsfG+5knMm7GHc0SrhkMeHiZdhWGHdON1w53THN+u23CxUtIrhpIc7kFnjaDsZ0JO68tiyL0LpfiPqL8eT3wZmWl9AeDQWoWvgN7hZd8eQfuEuvpTk09LOoSF1prdVNg8tHfPTRR/HrE088keeff562bdsm7JLt2LEjLpeLSCTC/PnzGzqliIiIiIiIiOxGLofJ7cf14MDW6fzli0KqI1a8rdgf4oKXf+KPI3tyxWGdav0p2R2JlK9i+d8PqNlgmCS1HUjmIdeS3v+8hCbfvK8oHf8vAsvGgb01FkdKLrknPUakch1FX/1xp3OnHngGrS94p17xApT//DqbvrwDK1CWcN/VogstT3+a5K5H7vQZoeKlbPzkOqoW/W/r+Jxu5Bz/V9J6nQpANFzN2ldOjr1O04lhOsk7/WkyBl4QH1P6/cOU/PgYHa7/qd6vQ0RkiwaXjygsLIxfv/LKK7Ru3bpGH8MwSE9Px7ZtioqKGjqliIiIiIiIiDSB43vn89plB9E2K/EnUqOWzV+/msd178zEH4zs+gSGA8OTBg432BbVq6ax/r1LqJz7WbxL6eTnWfv66QSW/gC2hSMtn6R2g3Gktybq30T16mm7Pn8dhUuWYQXKcOX2IKXXKbhyusfuFy9h7du/Ixr073B8NBRgzcsnUrXofxgOD8ndjsaR1pJw0SLWjT6PwMqpAJRPeY7AsnFkH/1/dLl3LY6UHDZ+eiPRcDUQ22VcOv6fZA79/V61S1hE9jwN3ilcUVEBQEZGBrm5udvt5/P5AIhGow2dUkRERERERESaSPeWaYy+/GDu+XQOExYlbvT68td1LFxfyfMXDqRzbv1/TDu521G0vexzrGiUsvH/pGjMnwAom/QMab1OIVJVTNEXt8c6my5anvkcaf3Oi5dcCCyfTLh0Oam9R5Ex5Jr4c1c9cyih9XMA6PznDRiOWHkMw9i1vXGe1v1oe/X/SO50KBArJbHmxWMJLJ+AVVWCf+EY0nufud3xvjkfEy5ZCkDO8Q+SdegNhIqXsfyxArCjlE74N97fjSa4bjYAKd2OxpGUgafNAPyFnxEpXoKjVS82fnYLpjebFsfcu0uvQ0RkiwYnhdPT0ykuLqa8vJzS0lKysrJq9Fm0aBHBYBDDMGptFxEREREREZE9V7rXxRPn9OOl8Ut5/selbHs40aKNPk59aiKPn92XY3u12qXnmw4H6QMviieFw6XLAKiY/iZ2NAhAxuBLE8ooAHg7DsXbcejmr1zx+9smf02nF9OVWDPZN/9rwqXLdxiTM6MdaQUnAZB24Gk14k1qN4jA8gmx+RzuHT6ratG38euUzaUi3C064c47gNCGuQSW/IAVjeJMiW22i1SsAyBaGau97EhrSeWcT6ha+F9anfM6jqT0Hc4nIrIzDU4KH3DAAUyYEPtD8M477+SFF16o0efee7d+B6ugoKChU4qIiIiIiIhIEzMNg6uGd6EgP50/fTKHyuqtZSMqgxGuenM61x3ZlZuP6Y7DrGudYRvLsrDDfsomPhW/687pBkDV4rHxe6mbk6mNoXzKC/jnf7XDPsndjoonhX8rWl1B5dxPAHCk5pHc9agdPmvLDmAAZ1p+/NqRlgcb5hL1byJauZaMg6+gbMrzbPz8FipmvkX1qqmk9jkLw5XMpi9ux9v5CFL7nl3HVykisn0NTgofc8wx8aTwyy+/zP/+t7VgellZGT169GDx4sXxe8cdd1xDpxQRERERERGRZnJot1xGX34wt73/C4s2+hLanvp+Mb+uKedf5/QjK2XHu2cBqhZ+y+K7vb+5a5A17EYArODW5ztScxoc+xZZh99CWr9zd9jHmV77rueIv4i1r51OpGQ5hstL/u9G43An7/BZdiRWExjDAebWVMyWshYAVjiAJ7c77W/4ifKfXiTqLyb31CfIGHwFxd/eT6RiHW0u+4zqFZMo/u99hEuX48ruTM6x92+zW1pEpG4anBS+5pprePzxx6msrMS2bZYti/2Ih23bBINBFi1aFO+bkZHBFVdc0dApRURERERERKQZtctO5rXLDuLBLwr5es76hLZxCzdx8lMTeO6CgRzYJqPOzzRcXrwdh5FxyB9I6XEMAM7UvHh7qGgRSa37NUr8ntb9cLfsteN4zJopk1DxMta8ejLhokUYnjTaXPQByZ0P3+l8pmdzuQc7ih0JgjuWCLdDWw+oc3hj5TY9ud3JO+nv8fvBTQsom/BvsoZdh+lOY+VTh+JIziL7iNspHvswa147lQ63/IIrfesOZBGRnWlwUjg3N5eXXnqJ8847D8uyarQbhoFt2zgcDl566SWys7MbOqWIiIiIiIiINDOvy8GDpx1I7zYZ/OPbhUSsrZWGV5cGOPPZSfz19N6MGth2u89I7jqC/As/AMPAcCZhmokHwaX2Og3fnI8BqJzxNul9zqrxDMuyaozbmfXvXFSn8hFtL9/aJ7DqZ9a+fjpR30Ycafm0ufSTOiepvR0OJrhmOgDh4sU48nvHrstWAuBq0QVnam6NcZZlsenTm3Ck5pB99P/h+/V97JCP9OE3kznkaiLlayn5/mECS37A1f+8OsUiIgKNkBQGGDVqFOnp6Vx77bUsWbKkRnuXLl148sknGTlyZGNMJyIiIiIiIiJ7AMMwOPeg9vRolcadH/5KkS8UbwtGLG57/xdmrSrl3pN64XbWkrg1nTg8Kdt9fuqBp+P46g6ilRvwz/+K9e9fRdbhN+PK7kKkbAWVv7yPbVvkHH1PveJOan8QGDtOJHu2SfhWLRrLmjdHYYf8mJ50ckY+iFVVStXi74FYUteV1R6AVc8fTWDZeBypeXS5ZxUAKT1PpGzSMwCUT34O96n/pmrRf4mUrgAgufsxtcbgm/sxVYvH0up3b+HwpGBbYWCbXcyO2OF6djRS63gRke1plKQwwLHHHsuCBQv46aefmDlzJmVlZWRmZtKvXz+GDBlS7+/aiYiIiIiIiMjeoX/7LN66Ygh3fvgrs1aVJbSNnrKSwrUVvHn5waR46peGMF0eWp//LmtePwMrUErF9NepmP56Qp/MYdfVO94WI+6qV3//om/jpR6sYAUb3r88oT3n+L+Rffgt2x3v7TqC5B4jqVowhvKpL1Ex613sUKxesiO1JdlH3FFjTDTop+iLO0juOoLUA88AILnLEeBwUznrPZLaDKDyl/9gOD14Ox9Wr9cjItJoSWEA0zQZOnQoQ4eqwLmIiIiIiIjI/iQ3zcPzFw7kn98u5N1pqxLaZqws49NZaxg1sB2G6cSV0x0AZ0abnT7X2/EQOtz4MxU/v07lr+8T2jgfsDGTs0nudBhp/WqWTXBmdcQKbznczWjwa3Ok5sVjro2ZnLV17sx2uHK640jeWj7TNE3yz3+Xson/pvynF4mUrcJwp5DW92yyj7gTVy3vQ8n//krUt4ncK76Ob7Rz53Sj1VkvUzTmT6x59RScWe1pec6ruLM7Nvg1isj+xbBt2955N9nTBQIBCgsLKSgowOv97cmtIlv5fD6i0SgOh4PU1NR6j/cHIxT5gqR4nJiN8I8r2XeZkSAGFjYmltOz8wEiu0hrTZqK1po0Fa01aSq7c619+es6/vplIdWRrWcPnX9we244qhupHifJbgfGLn6esKJRbCuMw5XUWOE2OSscBIdrhz9VHQ3GdiZvr7yGFQljOl27Jb7doaGfRUXqSmutbhq8U/iBBx6o95h77723odOKiIiIiIiIyB7qxD75dMtL5db3f2FNWQCAz39Zy3G9WtGjVRqRqE2ad9c2mpgOBzgcjR1ykzJdO0/C76jWMrBXJYRFZM/T4J3CpmnW+7t70Wi0IVNKLbRTWOpKO4WlqWiXkzQVrTVpKlpr0lS01qSpNMVaKw+EueeT2UxcXAyA0zQoaJ1ObpqHNple/nBEV1pl7L07fqXutHtTmorWWt002ulvtm3X6ZeIiIiIiIiI7B8yvC7+dW5/rjysMwARy+bX1eX8b95GZqws5dNZa1hVUtXMUYqI7H8a5aC5HSV7t91FrKSwiIiIiIiIyP7FNAx+f0QXClqn83+fzMEXjDC4Yxb/PKcfLkdsr5qvOkKKZ9frDIuISP00OCl88cUX13o/HA6zevVqfvrpJ4LBIIZhcMIJJ5Cbm9vQKUVERERERERkL3N491xGX3Ewd37wK388/gBcDhM77IeK1fgMB+G87mQkuTBNJYa3J1K5kWigGNOdiiuzXXOHIyJ7sQYnhV999dUdtq9fv57zzz+f77//no0bN/Lxxx83dEoRERERERER2Qu1z07m3auGYBgQjFhExt5D1fRX8BacinHaG5RUhfCWFlL633tqHe/O60neyY8DUFn4BeWTn93uXM70NrQ664WEe9WrplM6+VmqV0zGjgZxprYiqeNQ8k76+05jj4ar8c/9lMpZ7xDcMA/sKK6sTqT1PYvUvmfj9GbG+4aKl7Hxk+sSH2AYOJJzSO56FKm9T8PhSYs3RfxFlI57gqpF3xGpXI/h9OBMz8fdsoDMg68kqU3/2HPXz2H1y8djerPoeHshzuTsncYtIlKbRikfsSOtWrVi9OjRtGnThunTp/Pkk09yyy237O5pRURERERERGQPlJrkxGEYVK6fT/nMNwBwDbkZgKhlU1m2kapF39U61qquiF9Hyldttx+Aq0WXhK+L//cQxd/ev/WG4SBStorghrk7TQqHK9ax5uUTCW2Ym3A/UraKwLJxFP/vIdpc+hlJrfvE4gyWbze2ylnvUDL2r7S9eiyujNZYkRCrnj2ScNHCxGeXLqd6xWTcOd3iSeGkLkfgaTOQ4JrplH7/CLknPrLDuEVEtme3J4UB8vPzSU5OJhAI8OabbyopLCIiIiIiIrIfcpgGjs11g6unvQBWBFfeARit+tfaP+/0p2GbOsOO1Lz4dXKXI8g745mE/qH1cyibFLuX3PXo+P2qZRPiCeHkbkeTd+oTOLO7EK1Yg2/eVzuNe8MHV8UTwsldR5B9+O0Y7mTKJj1D5S/vEa1cx7q3zqXDLbMxHY6EsUntBpMx9PfYIR/lU18muPYXwiXLKJvwb3JPfBhf4afxhHDGwVeTPvBCDIeT0KaF+Bf+F3ObHcWmaZI+4HdsWjOd8p9fI/uY+3C4vTuNX0Tkt5okKfzqq69SVRU7TXTRokVNMaWIiIiIiIiI7GFcm+sF27aN79f/AJDS8wQcDoNItObh9NFup5FKFc60PByupIQ2T94BePIOSLi39q3zYheGg6zhN8XvV8wYDYDpSafV796Kl3owM9uRNfTqHcYcKl1B1cJvAXBmtqP1JZ9iOt2xGNodRLh8NdXLJxIuXkL10h9J7jYiYbwzsx0ZA84HwNthGCv+NRCAqsX/AyBctCTeN+Pgy0hq3Q+ApDb9Se93To14UnudxqbPb8UKlOEv/Jz0fmfvMH4Rkdo0OCk8YsSIWu/btk0oFGLdunWsWLECwzCwbRuns0ny0CIiIiIiIiKyh3E4Yknh0PrZRP1FACS1P5gUrxtfMEJ1OJrQv/jRthRjY7iSSe5+NLnHP4Q7p1utzw4VL8M351MAUnudgrtF53ibf/6Y2Pypuax/6zwCK6dimA6Sux1FixF/wpN/4HZj9s3+GIglrNN6j4onhGHzzt3+v6N6+UQAKud8XCMpvK1I5fr4teFKBmJ1krdY/cKxJHc7mqR2B5HS/RhceQdgmmbCM1yZbXFmtCFSvobA0h+VFBaRXdLgDO0PP/yAYWz/ZFDb3vqdPsMwGDx4cEOnFBEREREREZG9kLk5fxDatCB+z5XdCQNI8zhxOQy2VA02vdmYKblEy1dhh6vwz/2M6lU/0/G2wlpLJpROeALsWFI5a/it8ftWNEK0ch0A4eIlhEuW4UhrRbRiLb7ZH1G1eCwdbpqBK6NNrTFHylfHr51Z7Wu0u7I6bu1bsbZGe/Xq6az7z+XYIT/+beoMp/UZBUDKASeT0vN4/PO/xqouxzf7Q3yzP6ToqzvxtO5Pq7NeqpG0dma0I1K+hlDR4lpjFhHZGXPnXRrGMIx40tg0Te6+++7dPaWIiIiIiIiI7IG2bCmzqsvi98ykjPh1ktNBeusDyLxiHKk3LSXlqqmk37AAT+cjAGKJ3F/fr/HcSFUJFdPfBMDb8VC87bfdkGZsMzO0Ou9Nuty9jLzTn4rFEiijfOor24/ZubVshR3y12i3Qr5aX0s8ttIVVM4YjW/Ox9jBSgBS+5xFxsFXxcY4HORf9BH5F7xHWr9zcWa2i48Nrp3J2rd/h2VZCc80k9Jjc1eXbjduEZEdaZRaDtvuBt6eDh068K9//YsjjzyyMaYUERERERERkb3MluyB6UmP37NClQl9krLa4c5sS2V1hFDUAk867n6XEFz6AwD+Nb+QMsDCuU1ZhfJJz8YTtpnb1BKGWNLVmdWeSOkKAFIPOBmAlANOgo+vAyC0oXC7MSe1GxS/rl77S4326tXT49e1lbZwt+pNer9zwTBxpOSS3G1EjV3JpmmSduBppB14GgDB9XNZO/ocwkWLCG9aQLRyPWZG63h/a3Nyedv3UUSkPhqcFH711Ve322YYBsnJyXTt2pW+ffvusMyEiIiIiIiIiOzbLNvGYRi4crrH70VKluPJ6xX/OrB8AkltB5Pu9VAVilAVimKtn7n1ISktKfGHyEhy4XE5iIarKZvyPADu3J6k9DyxxrzJXUdQMS2Wv4j6N2FmtsXyF8fbnZlttxtzSs8TMJOzsapK8M39lOo1M0lq0x+AUMlyyqe+FOtoOEjffKDctty53cg+4rbtPt+/aCzR6lJSDzh56wF2rXrhzulGuGhRrWO2lLRwZXeutV1EZGcanBS++OKLGyMOEREREREREdnHRS0blwme/L6Y3kysQBnVq6YlJHJLx/2D4KqpJHc7FmdGG8IbCqma/xUAhtODeeDvsG0oC4RJsWwis94k6tsAQOZhN9Q4mA0gY/BlVPz8OtgW6z+4grTeo+LlJjBMMgZfut2YTaebFkfdw6bPb4FoiJVPH0pyt6MwnB6qFnyDHQkC0OKY/8NVS83hnQkXL2LjJzdgerNIajsQR0oO4ZLlVK+cAkBS28G4ttklHK5YR6RsFQDezsPrPZ+ICDRS+QgRERERERERkZ2JRG1wgmGapPY+k4qpL1M1/2taHHNfvI8rvTVV/iIqZ72dMNZMySX1pCcxknPi93zVYQLj/w2AI60l6f0vqHVeb/uDaHnGs2z6+i4Ci78nsPh7AAx3CrknPYanZcEO484adi2Gy0vpD38nXLKUqgXfxNsMdyo5x95HxiHX1uu92MKR3gYzKQMrUErVNgfRATgz2pB76hMJ93xzP4vN60kj9cBTd2lOERHDrktB4J345ZdfGDduHKtXr6asrIzk5GS6dOnC8OHD6dOnT2PEKTsRCAQoLCykoKAAr7fmKawiW/h8PqLRKA6Hg9TU1HqP9wcjFPmCpHic8ZODRWpjRoIYWNiYWE5Pc4cj+zCtNWkqWmvSVLTWpKk011pLT4p9lgiun8OqJw8C26LdDT/jaXVgvE+4bCXVK38iWrEGTAfu/L4kdTgEX8gmGNl66JodqYZ1P2MaBimZrUhp3au2KeOioSr8cz8lUrkBZ2YbUnqcgMOTUufYLcsiuGoawXW/ULXke3yzP8LwpNHuyv+S1HbA1nmClQQ31xp2pObtNOlsRcIE184itGEu0UAphsNFUpuBeNoNxnQk7udb+czhVK+cQsbQa2h56r/qHHtza+hnUZG60lqrmwYlhb/66ivuvvtuZs+evd0+/fv357HHHuOII46o0eb3+0lJqfsfvrJ9SgpLXSkpLE1FH2ilqWitSVPRWpOmorUmTaW51prHaeJ1OQDY8P7lVM58i9R+59Lq7NfqNL5wXQWPjlnAI6N6J3wmMYA0ryv+7N3NikZY99Z5+As/w5Hakna//wF3i91b47dq2QRWP38UpiedjrfNxZmWt1vna0xK1ElT0Vqrm11OCj/wwAPcf//9AOzsEaZp8re//Y3bb789fq+kpIQTTjiBKVOm7Mr08htKCktdKSksTUUfaKWpaK1JU9Fak6aitSZNpTnX2pbdwpHqUsIb52OYLpLaDgIgattYlo1pGpiGgY2NbUM4ahGO2kQtm5+Xl/Cfn1dzePdcslJcAISjNjNWlOJ2mvzx+J64HDVrCze2aChAxdSXsO0orpxupB1Q85C7xhQqXUG0fC1mchaevJ67da7GpkSdNBWttbrZpZrCr732Gvfddx8AhmFg7CQxZFkWf/zjH8nPz+eCCy5g3bp1HHPMMcybN29XphcRERERERGRvVhVOEqq24kzKQtn+6Hx+xY2vuoIO9u9NqhjNu2yk7n9g1+Zs6a8Rvuvq8t4+ncDyEtPauTIEzncXrIOvX63zrEtd1YHyOrQZPOJyL6r3t828/l83HLLLQnJYNu2sW2bjIwMWrduTUZGRvwexBLHtm1zww038PPPP3PooYdSWFjYuK9ERERERERERPYKkaiNLxghFLUIW7FfwYhVp4TwFi3Tk3jpokGMGti2Rtu05aWc9OQEfl5e0riBi4jsI+qdFH7ttdcoKysDYsngDh068OKLL7JhwwZKSkpYtWoVJSUlbNiwgRdeeIEOHTpg2zaGYVBWVsbQoUNZtmxZbHJz9/8oh4iIiIiIiIjseSKWTVUoij8Y+xUIR7HqWeDS7TS5+4QD+PPJBbh/Uy5iY2WQc1+YwuuTlu+07KWIyP6m3lnZ7777Ln49YMAAZs2axeWXX05ubm5Cv9zcXK644gpmzpxJv3794onhaDQKgMvl4u23325g+CIiIiIiIiKyvzu1XxteuXQw+RmJ5SIils2fP5vLLf/5hUAo2kzRiYjseeqdFJ41a1b8+sknnyQ9PX2H/TMyMnjyyScT7iUnJ/PZZ59x1lln1Xd6EREREREREZEaCvLTGX3FwQzpnF2j7eOZazjj2UmsLK5qhshERPY89U4KFxcXA5CUlMTQoUN30jtm2LBheL3e+G7hb7/9luOOO66+U4uIiIiIiIiIbFdWspsnzxvApcM61mibt66Ck5+awPcLNjZ9YCIie5h6J4UjkQgQK/9QH263G6hfMllEREREREREpD4cpsH1I7rx2Fl9SXE7EtrKA2Eue20a//7fIqz6FjAWEdmH1Dsp3KJFCwAqKyvjB8btzIoVKygvL8cwjPh4EREREREREZHdZUTPPN68/GA65aQk3Ldt+Me3C7nqzZ8pD4SbKToRkeZV76Rwjx494td/+tOf6jTmvvvuq3W8iIiIiIiIiMju0jEnhTcuO4ijD8ir0fbdvI2c+tQE5q+vSLg/b10F//fJHM54ZiIjnxjHGc9M5P8+mVOjn4jI3sxZ3wFHHHEE33//PQDvvfcekUiERx55hE6dOtXou3r1au6++27eeuuthPEiIiIiIiIiIk0hxePkkTP78OaUFbGyEdtUjVheXMXpT0/ikVF9aJ+dzINfFvLz8lIADMDe/PuMlWW8OWUFgzpmcc+JBfRrl9kMr0REpPEYtm3Xq4jOmjVr6Ny5M5FIJH5wHEBBQQE9evQgJSWFqqoqFi5cyNy5c7Ftmy1TuN1uli5dSuvWrRv/leznAoEAhYWFFBQU4PV6mzsc2YP5fD6i0SgOh4PU1NR6j/cHIxT5gqR4nJib//sXqY0ZCWJgYWNiOT3NHY7sw7TWpKlorUlT0VqTprI/rrWpy0r440e/UlZVs2yEacRKS+woSWIAbqfJsxcMYETPlrstzn1RQz+LitSV1lrd1Lt8RJs2bbjpppviCeEtSd+5c+fy8ccfM3r0aD766CNmz56NZVnxfoZhcNNNNykhLCIiIiIiIiLN4qBO2bx9xRAObJ1eo83aSUIYYu2hiMXvR89g1qqy3RGiiEiTqHdSGOChhx7i+OOPT0j4bpsgru3+cccdx0MPPdTY8YuIiIiIiIiI1FmrjCReungwZwxos0vjtySG//plYeMGJiLShHYpKexwOPj888+566678Hg8CSUitpST2HLP7XZzxx138Pnnn2OauzSdiIiIiIiIiEijcTtN7jmxgKuHd96l8TYwbXmpDp8Tkb1WvQ+a28I0Tf76179y/fXX89577/Hjjz+ycuVKKioqSE9Pp127dhx++OGcc8455OfnN2bMIiIiIiIiIiINVlIVatD4t6as5C+nHdhI0YiINJ1dTgpv0apVK2688UZuvPHGxohHRERERERERKRJLFhficHOawnXxgAK12mnsIjsnVTPQURERERERET2S1Wh6C4lhCGWSPYHI40ZjohIk1FSWERERERERET2S8luB8YujjWAFE+DfwBbRKRZKCksIiIiIiIiIvulHq3SGrRTuCA/vTHDERFpMkoKi4iIiIiIiMh+6cwBbRs0fkjn7EaKRESkaSkpLCIiIiIiIiL7pe4t0+jXLnOXx9/47iz+9d0iwlGr8YISEWkCSgqLiIiIiIiIyH7rlmO643aau1RbOGLZ/PO7hZzxzCQWbqhs9NhERHYXJYVFREREREREZL91YJsM/j6qDy5H3VIktSWPZ68p56R/T+DZH5YQ0a5hEdkLKCksIiIiIiIiIvu1w7rl8tLFg+i/TSkJ4ze/A/Rvl8nLlwzi8kM74TAS08OhqMUjY+Yz6rnJLNnk2+0xi4g0hLO5AxARERERERERaW4Htsng5UsGs2hDJR/MWM3C9ZX4Q1FS3A66t0pj1IC2dGuZBkC/dlkc0SOXez+dy7Iif8JzZq0q44R/jef243pw2bBOmOauFKYQEdm9DNu27eYOQhouEAhQWFhIQUEBXq+3ucORPZjP5yMajeJwOEhNTa33eH8wQpEvSIrHiWnoHzeyfWYkiIGFjYnl9DR3OLIP01qTpqK1Jk1Fa02aitZawwUjUZ79YQlvTl5BbcmVgzpm8/ez+tChRUqTx7anaehnUZG60lqrG5WPEBERERERERHZBR6ng5uO7s4rlwymfXZyjfapy0sY+cR43pi8HMvSnjwR2XMoKSwiIiIiIiIi0gB922XyzlVDOO+gdjXaAuEo9346lwte/onVpVXNEJ2ISE1KCouIiIiIiIiINJDX5eD243ry4kUDaZNZs6zjpCXFjHxiPO9OXYkqeYpIc1NSWERERERERESkkQzskM17Vw9h1MC2Ndp8wQh//Gg2l7w6jXXlgWaITkQkRklhEREREREREZFGlOx2cvcJB/DM+QNolZ5Uo/3HhZs49p/j+HD6au0aFpFmoaSwiIiIiIiIiMhuMKRzC967egin9mtdo62yOsKt7//ClW9MZ2NldTNEJyL7MyWFRURERERERER2k7QkF38+uRf/OrcfOanuGu3fzdvAsf8cx+e/rG2G6ERkf6WksIiIiIiIiIjIbnZYt1zev+YQTuidX6OtrCrM9e/M5Nq3ZlDsCzZDdCKyv1FSWERERERERESkCWR4XTx42oE8dlZfslNq7hr+cvY6jv3nOMbMWd8M0YnI/kRJYRERERERERGRJjSiZx7vXz2UYw5oWaOt2B/imtHTuendmZRVhZohOhHZHygpLCIiIiIiIiLSxLJS3Dwyqg8Pn9GbTK+rRvsns9Zy7D/HMXb+hmaITkT2dUoKi4iIiIiIiIg0k2N7teI/1wzliO65Ndo2Vga57LWfuf39X6ioDjdDdCKyr1JSWERERERERESkGeWkenj87L48cGovUj3OGu3vT1/Ncf8cx7iFm5ohOhHZFykpLCIiIiIiIiLSzAzD4KQ+rfngmqEM69qiRvu68mouemUqd388G18w0gwRisi+RElhEREREREREZE9RF56Ev8+tz/3nlRAittRo/3tn1Yy8olxTF5S3AzRici+QklhEREREREREZE9iGEYnNa/De9dPZSDOmXXaF9dGuC8F6dw32dzCYSizRBh/RmGgdPpxOmsWR5DRJqeksIiIiIiIiIiInug1plenjl/AH88vidJrpopnNcmLef4f41j+oqSZoiufrxeL16vF7fb3dyhiAhKCouIiIiIiIiI7LFMw+DsQe1476qh9G+fWaN9eXEVo56bzENfzaM6vHfsGhaR5qc9+yIiIiIiIiIie7h22cm8eNEg3pm6kqfGLiYYseJttg0vjFvK2PkbefysvvRtl7nb4igZ/y98cz5OuGeYLlw5XUktOJnkHiMxzcQ9iNXrZlMxfTT++V8R9W/EcHhw5/UktdepZBx8Jevfu5hIxbqdzp132pMk5feud8ylE56kcvaHtbZlDL6cjEEX7vQZodIVlI3/F765n2KFq/C07EXmsOtIKTgl/nrDFRvY9MUtBJZNwHSnkN7/fLKO/COmI1Yb2rIs1r1xBqHipbS//iccbm+9X4tIY1FSWERERERERERkL2AaBucf3IFhXXL482dzmb2mPKF98UYfZzw7iWsO78wN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m29i7dq1uxTf5MmTufDCC+japTMpyV6yMjPo3q0rI487lr888ABz5szZ1ZfeIF27dMbldOByOhg/btx2+23p4/P5Eu4fNWJEvO2NN17f6Ty78jrnzZuHN8nD9ddft8N+r7/+WjyWp59+ut7z7ImWL18ef00up4OJEydut29JSQkpyd543y+/+GK3x/fA/ffjcjp44P77G/ysefPm4XG7uOWWmxshMhERERERaW5VoehOD5fbHhvwByONGY7sp5QUlt1m1qoyRj03ieP/NZ43p6xg5soy5q+vZObKMt6csoKRT4xn1HOT9uiTM99//338fj8A06dPb7YEZWO5+OKLueiiizj33HM57LDD8Hq9fPDBB1x99dW0adOGF198cYfju3TpwkUXXcwFF17EhRddxOGHH86mTZt46sknGTigPwsWLKhXPI8/9hiHDz+Md995B5fLxbHHHstJJ51E69ZtmDx5Mg88cD9vvL79hGpTueeeexo0/sG//IVwONxI0Wx1x+234XK5uPvuP+2w3+uvbX0Pd5Sgbogtye3ly5fvlufvzOjRb2637d133yUUCjVhNI3rgAMO4NzzzuO5Z59l4cKFzR2OiIiIiIg0ULLb0aCdwikeZ2OGI/spJYVltxg7fwNnPz+Z6ctL4/fs3/wOMH15Kec8P5mx8zc0aXx19dprrwHQunXrhK/3Vq+99hqvvPIKr776Kh9++CEzZ85k3bp13HHHHfj9fq666ioeffTR7Y4/9NBDeeXVV3nplVd4+ZVX+eLLr1i0eAnDhx9OUVERd9/1xzrHMmvWLO666484nU7eefdd5s1fwMeffMqbo9/ihx9/ZP2Gjbz73nv069+vEV75rktOTmbSpIl89eWXuzx+2bJlO02419cP33/PmDFjuOzyy8nPz99uv6VLlzJhwnhSUlLIyMhgxj7wzY1tZWdn06lTJz54/32qq6tr7fPGG6/jdrs5sHfvJo6u8fzpT/cQDof5v3t2/A0AERERERHZ8/VoldagncIF+emNGY7sp5QUlkY3a1UZ14yeQThi7fQPORsIRSx+P3rGHrdjeOnSpYwfH0umvfrqqwC89dZbRCL71o9p5Obm8sgjj/DCCy8AcNddd9Vrx296+v+3d+dxUVXvH8A/wzZssoOoiCgKuHzV1FJxAdRMxcIFl2R1qczdsn6puZuW+5K5lKngUm6h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iIi0CJPCRERERERERERERFqESWEiIiIiIiIiIiIiLcKkMBEREREREREREZEWYVKYiIiIiIiIiIiISIswKUxERERERERERESkRZgUJiIiIiIiIiIiItIiTAoTERERERERERERaREmhYmIiIiIiIiIiIi0CJPCRERERETV1Ny5cyESieRfwcHBCu1bt25VaPf09NTIPImIiIioZmFSmIiIiIiqNScnJ4XE5/NfhoaGqFevHnr06IElS5YgPT1d09OtdYqKipCUlITc3NzXcuzw8HDMnj0bffv2hY2NjdJzXB6CIODnn3/Ge++9h/r160MsFsPW1hZdu3bF8uXLkZeXp/a5ExEREdVkepqeABERERFRZRUUFCA5ORnJycmIiorC8uXLcfToUbRt21bTU6vRTp06hZ07dyIiIgKJiYkQBAEAYGZmhnbt2mHw4MEYNmwYbG1tX2mcR48e4d13332lY2RlZWHAgAGIjIxU2J6amorU1FScPXsW3377LSIiIuDq6vpKYxERERHVFlwpTEREREQ1iomJCerWrQtLS0ultpSUFLz//vvyJCZVzJ07d9CzZ094eHhg48aNSEhIUPheZmZmIjo6GhMnTkTTpk2xZs0aSCQSjc1XEAQMGjRIKSFsbm4OHZ3//tSJj4+Hl5cX0tLSqnqKRERERNUSk8JEREREVKNMmzYNycnJSEtLQ0pKCnx8fBTa7969iytXrmhodjXXgQMH0LFjR0RGRsLAwACBgYEICwvD33//jYKCAvz777+4fv06vvnmG7Rs2RKZmZmYPHkyhg4diuLi4kqNaWBggD59+mDWrFk4ePAgzp07V6H+O3fuxPHjx+WPLSwscO7cOaSnp+Pvv/9Gu3bt5G1JSUmYOXNmpeZJREREVNswKUxERERENZatrS2WLVumtD0uLq7UPkePHoWfnx+cnZ1hYmICIyMjODs7Y+TIkbh8+XKZ4wmCgCNHjsDf3x8uLi4wMzODWCyGg4MDvLy88NVXXyEjI0O+f15eHtatW4fRo0ejQ4cOcHR0hKmpKQwMDGBra4suXbpg7ty5SEpKqvw3QQ2ioqIwZMgQZGVloWPHjrh79y62bdsGHx8fODg4wMDAAFZWVmjdujU+//xzXL9+HWvWrIG+vj72798Pf3//So1bv359HDlyBPPnz8e7774Le3v7CvX/7rvvFB5/+umn6NSp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\n" + }, + "metadata": {} + } + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "name": "scann_on_alloydb_glove_100_angular.ipynb", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "version_major": 2, + "version_minor": 0, + "state": { + "bb2bb0d3c235439a8dd8b8cb9f544191": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_f0d4e9e968724c098b56d8733e8cb42a", + "IPY_MODEL_9b904fc7fefd4ed2b7282dfff26e8244", + "IPY_MODEL_631658ca63714d60a1cfadf61adde2f3" + ], + "layout": "IPY_MODEL_78d279973dad446b9b85635c724c0455" + } + }, + "f0d4e9e968724c098b56d8733e8cb42a": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a224f47dfa8d40cfaafd52c30741db33", + "placeholder": "​", + "style": "IPY_MODEL_eb975b210f304ea6a2d9ae8081e46a75", + "value": "Inserting 1,183,514 Training Records: 100%" + } + }, + "9b904fc7fefd4ed2b7282dfff26e8244": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e510a1e31be54e3b82832f1047768ad1", + "max": 237, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_1a928fcb6f964fdca0fe3d08b5141224", + "value": 237 + } + }, + "631658ca63714d60a1cfadf61adde2f3": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0cde9f71448f40ba9f33d63d97f25bbf", + "placeholder": "​", + "style": "IPY_MODEL_5038b51e1d454b5e85f92bc905c9e5c3", + "value": " 237/237 [02:47<00:00,  1.71it/s]" + } + }, + "78d279973dad446b9b85635c724c0455": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a224f47dfa8d40cfaafd52c30741db33": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "eb975b210f304ea6a2d9ae8081e46a75": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "e510a1e31be54e3b82832f1047768ad1": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1a928fcb6f964fdca0fe3d08b5141224": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#1f77b4", + "description_width": "" + } + }, + "0cde9f71448f40ba9f33d63d97f25bbf": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5038b51e1d454b5e85f92bc905c9e5c3": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "0f1b3c48bca840a9ade73b114aa97033": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_bd95243e64284400ae94adda764dd676", + "IPY_MODEL_4211e3547ca54b80ab8bd83cd18205ed", + "IPY_MODEL_722cad3215fe4f8aac048fe76301f882" + ], + "layout": "IPY_MODEL_e1d193b7e60f4914a5477d618b1d2ec5" + } + }, + "bd95243e64284400ae94adda764dd676": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_0a6cad141bb543ff85abe1752e92f1f0", + "placeholder": "​", + "style": "IPY_MODEL_42ece952d02b44c49a521f7181d6d039", + "value": "Inserting 10,000 Test Queries: 100%" + } + }, + "4211e3547ca54b80ab8bd83cd18205ed": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7cae70365604497db1f33470ca3e329f", + "max": 2, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_1f96e9c7b40e41f4882af89dae656820", + "value": 2 + } + }, + "722cad3215fe4f8aac048fe76301f882": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_4510043017864ff8ad0e38e33494b5fe", + "placeholder": "​", + "style": "IPY_MODEL_6f0d2aada81f42e3b645ba0c78f6f2dc", + "value": " 2/2 [00:01<00:00,  1.56it/s]" + } + }, + "e1d193b7e60f4914a5477d618b1d2ec5": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0a6cad141bb543ff85abe1752e92f1f0": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "42ece952d02b44c49a521f7181d6d039": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "7cae70365604497db1f33470ca3e329f": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1f96e9c7b40e41f4882af89dae656820": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#1f77b4", + "description_width": "" + } + }, + "4510043017864ff8ad0e38e33494b5fe": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6f0d2aada81f42e3b645ba0c78f6f2dc": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b7489eb0b544467794893578b208fc30": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_2009e650f2e544d2bd1a87bdb80f4f17", + "IPY_MODEL_4b014493a650429295b29f5e860c5cf8", + "IPY_MODEL_25d9691c62b745148f36c3923f457e00" + ], + "layout": "IPY_MODEL_1dd50d6fd07449ddbe838f17bf04d557" + } + }, + "2009e650f2e544d2bd1a87bdb80f4f17": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_452d933c977342b9b7809a62bcb7114d", + "placeholder": "​", + "style": "IPY_MODEL_66b1ecc6a2ee4e9ab9186b4af0ee5fa0", + "value": "Benchmarking ScaNN Auto Search (% Leaves): 100%" + } + }, + "4b014493a650429295b29f5e860c5cf8": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_dd57d3e0dfa84249a220d1472a88dfdc", + "max": 7, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_79c81933da0c4dcfa8a2130f77c1a281", + "value": 7 + } + }, + "25d9691c62b745148f36c3923f457e00": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e956f439a7ef4b16a5a01b6c3968920b", + "placeholder": "​", + "style": "IPY_MODEL_32f09bc5af3e4bfebd4abd146656b47a", + "value": " 7/7 [00:32<00:00,  4.94s/it]" + } + }, + "1dd50d6fd07449ddbe838f17bf04d557": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "452d933c977342b9b7809a62bcb7114d": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "66b1ecc6a2ee4e9ab9186b4af0ee5fa0": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "dd57d3e0dfa84249a220d1472a88dfdc": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "79c81933da0c4dcfa8a2130f77c1a281": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#1f77b4", + "description_width": "" + } + }, + "e956f439a7ef4b16a5a01b6c3968920b": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "32f09bc5af3e4bfebd4abd146656b47a": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + } + } + } + } + }, + "nbformat_minor": 5, + "nbformat": 4 +} \ No newline at end of file diff --git a/alloydb/notebooks/columnar_engine_accelerated_hnsw_vector_search_benchmark.ipynb b/alloydb/notebooks/columnar_engine_accelerated_hnsw_vector_search_benchmark.ipynb new file mode 100644 index 00000000000..bdd937816f2 --- /dev/null +++ b/alloydb/notebooks/columnar_engine_accelerated_hnsw_vector_search_benchmark.ipynb @@ -0,0 +1,2728 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "Copyright 2026 Google LLC\n", + "\n", + "Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "you may not use this file except in compliance with the License.\n", + "You may obtain a copy of the License at\n", + "\n", + " https://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software\n", + "distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "See the License for the specific language governing permissions and\n", + "limitations under the License." + ], + "metadata": { + "editable": false, + "id": "87m82ztIVg1L" + } + }, + { + "cell_type": "markdown", + "source": [ + "# 🚀 Columnar Engine Accelerated Hnsw Vector Search Benchmark\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/GoogleCloudPlatform/python-docs-samples/blob/main/alloydb/notebooks/columnar_engine_accelerated_hnsw_vector_search_benchmark.ipynb)\n", + "\n", + "---\n", + "This interactive notebook measures the performance impact of **AlloyDB's Columnar Engine** on **pgvector HNSW** indexes. Visit [Get Started with AlloyDB](https://cloud.google.com/alloydb/docs/quickstart).\n", + "\n", + "*(Note: The performance metrics demonstrated in this notebook were captured using an **AlloyDB C4A 16-vCPU** instance. Your results may vary based on machine type & size).*\n", + "\n", + "🚨 **IMPORTANT PREREQUISITES:**\n", + "\n", + "Before running this notebook, verify the following requirements:\n", + "1. **PostgreSQL 17+**: Ensure you have an **AlloyDB for PostgreSQL 17 (or greater)** cluster created. (HNSW Columnar Engine caching is not supported on PG14/PG15/PG16).\n", + "2. **Public IP** is enabled on the instance (Note: there is no need to authorize any networks!).\n", + "3. The following **Database Flags** are configured on your AlloyDB primary instance:\n", + " * `google_columnar_engine.enabled` = `on`\n", + " * `google_columnar_engine.enable_index_caching` = `on`\n", + " * `google_columnar_engine.memory_size_in_mb` = `1024` (or larger)\n", + "\n", + "📚 **Helpful Resources:**\n", + "\n", + "* [Get Started with AlloyDB](https://cloud.google.com/alloydb/docs/quickstart)\n", + "* [Accelerate vector search with the columnar engine](https://docs.cloud.google.com/alloydb/docs/ai/accelerate-with-ce)\n", + "* [pgvector](https://github.com/pgvector/pgvector)\n", + "---" + ], + "metadata": { + "id": "markdown-header" + } + }, + { + "cell_type": "code", + "source": [ + "# @title ⚙️ Benchmark Execution\n", + "# ==========================================\n", + "# 1. CONFIGURATION\n", + "# ==========================================\n", + "# @markdown ### **⚙️ 1. Cluster Configuration**\n", + "project_id = \"\" # @param {type:\"string\", placeholder:\"Project ID\"}\n", + "region = \"\" # @param {type:\"string\", placeholder: \"Region (e.g. us-central1)\"}\n", + "cluster_name = \"\" # @param {type:\"string\", placeholder: \"Cluster name\"}\n", + "instance_name = \"\" # @param {type:\"string\", placeholder: \"Instance Name\"}\n", + "\n", + "# @markdown ### **🔐 2. Database Credentials**\n", + "# @markdown *Note: Password will be requested securely when the benchmark is executed.*\n", + "db_user = \"\" # @param {type:\"string\", placeholder: \"DB User\"}\n", + "db_name = \"\" # @param {type:\"string\", placeholder: \"DB Name\"}\n", + "\n", + "# @markdown ### **⚙️ 3. Benchmark Settings**\n", + "# @markdown *Note: Increasing these values will provide more intensive testing, but will slow down the benchmark runtime.*\n", + "# @markdown * `test_queries`: The total number of test queries to run during the benchmark. Increasing this value provides a more stable average but slows down the overall runtime.\n", + "# @markdown * `search_limit`: The number of nearest neighbors to retrieve per query (i.e., the `LIMIT` on the vector similarity search, equivalent to top-k).\n", + "\n", + "test_queries = 1000 # @param [100, 1000, 10000] {type:\"raw\"}\n", + "search_limit = 10 # @param [10, 100] {type:\"raw\"}\n", + "vector_dim = 100 # Fixed for GloVe-100 dataset\n", + "\n", + "import getpass\n", + "# Prompt for password upfront so user doesn't wait for the pip installation\n", + "db_pass = getpass.getpass(\"🔑 Enter Database Password for AlloyDB: \")\n", + "\n", + "# ==========================================\n", + "# 2. SETUP & AUTHENTICATION\n", + "# ==========================================\n", + "import os\n", + "print(\"📦 Installing dependencies silently... (This takes ~15 seconds)\")\n", + "%pip install -q h5py matplotlib asyncpg google-cloud-alloydb-connector[asyncpg] tqdm rich pgvector\n", + "\n", + "print(\"🔐 Authenticating with Google Cloud...\")\n", + "from google.colab import auth\n", + "auth.authenticate_user()\n", + "\n", + "from IPython.display import clear_output\n", + "clear_output()\n", + "print(\"✅ Output cleared. Colab Setup successful!\")\n", + "\n", + "# ==========================================\n", + "# 3. LATE IMPORTS (Post-Installation)\n", + "# ==========================================\n", + "import asyncio\n", + "import urllib.request\n", + "import shutil\n", + "import h5py\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.ticker as ticker\n", + "from tqdm.notebook import tqdm\n", + "from rich.console import Console\n", + "from rich.panel import Panel\n", + "from rich.markdown import Markdown\n", + "from google.cloud.alloydb.connector import AsyncConnector\n", + "import pgvector.asyncpg\n", + "from pgvector.asyncpg import register_vector\n", + "from IPython.display import display, HTML\n", + "\n", + "console = Console()\n", + "\n", + "def print_step(step_label, text=\"\", is_success=False, is_error=False):\n", + " # Base styling (font size and spacing)\n", + " base_style = \"font-size: 17px; margin-top: 10px; margin-bottom: 4px;\"\n", + "\n", + " if is_success:\n", + " # Success: Fully bold, Universal Material Green\n", + " display(HTML(f'''\n", + "
\n", + " {step_label} {text}\n", + "
\n", + " '''))\n", + " elif is_error:\n", + " # Error: Fully bold, Universal Material Red\n", + " display(HTML(f'''\n", + "
\n", + " {step_label} {text}\n", + "
\n", + " '''))\n", + " else:\n", + " # Steps: Label is bold and colored; Text is normal weight and theme-adaptive\n", + " display(HTML(f'''\n", + "
\n", + " {step_label}\n", + " {text}\n", + "
\n", + " '''))\n", + "\n", + "# ==========================================\n", + "# 4. SQL DEFINITIONS (Optimized with Array Intersection)\n", + "# ==========================================\n", + "SQL_MEASURE_RECALL = \"\"\"\n", + "CREATE OR REPLACE FUNCTION measure_recall(ef INT, num_q INT) RETURNS FLOAT AS $func$\n", + "DECLARE\n", + " q_rec record; total_recall FLOAT := 0; retrieved_ids INT[]; intersect_count INT;\n", + "BEGIN\n", + " PERFORM set_config('hnsw.ef_search', ef::text, false);\n", + "\n", + " FOR q_rec IN SELECT embedding, ground_truth FROM glove_test LIMIT num_q LOOP\n", + " SELECT array_agg(sub.id) INTO retrieved_ids FROM (\n", + " SELECT id FROM glove ORDER BY embedding <=> q_rec.embedding LIMIT __SEARCH_LIMIT__\n", + " ) sub;\n", + "\n", + " -- Instant Intersection of retrieved array and ground_truth[1:search_limit]\n", + " SELECT count(*) INTO intersect_count\n", + " FROM unnest(retrieved_ids) as r\n", + " JOIN unnest(q_rec.ground_truth[1:__SEARCH_LIMIT__]) as g ON r = g;\n", + "\n", + " total_recall := total_recall + (intersect_count::FLOAT / __SEARCH_LIMIT__);\n", + " END LOOP;\n", + "\n", + " RETURN total_recall / num_q;\n", + "END;\n", + "$func$ LANGUAGE plpgsql;\n", + "\"\"\".replace(\"__SEARCH_LIMIT__\", str(search_limit))\n", + "\n", + "SQL_MEASURE_QPS = \"\"\"\n", + "CREATE OR REPLACE FUNCTION measure_qps(ef INT, num_q INT) RETURNS FLOAT AS $func$\n", + "DECLARE\n", + " start_time timestamp; end_time timestamp; q_vec halfvec;\n", + "BEGIN\n", + " PERFORM set_config('hnsw.ef_search', ef::text, false);\n", + "\n", + " start_time := clock_timestamp();\n", + " FOR q_vec IN SELECT embedding FROM glove_test LIMIT num_q LOOP\n", + " PERFORM id FROM glove ORDER BY embedding <=> q_vec LIMIT __SEARCH_LIMIT__;\n", + " END LOOP;\n", + " end_time := clock_timestamp();\n", + "\n", + " RETURN num_q / (extract(epoch from end_time) - extract(epoch from start_time));\n", + "END;\n", + "$func$ LANGUAGE plpgsql;\n", + "\"\"\".replace(\"__SEARCH_LIMIT__\", str(search_limit))\n", + "\n", + "# ==========================================\n", + "# 5. CORE FUNCTIONS\n", + "# ==========================================\n", + "async def verify_columnar_flags(conn):\n", + " print_step(\"Step 2/9:\", \"🔍 Verifying PostgreSQL Version & Columnar Engine Flags...\")\n", + "\n", + " # 1. Verify PostgreSQL Version (Requires PG17+)\n", + " pg_version = await conn.fetchval(\"SHOW server_version_num;\")\n", + " if not pg_version or int(pg_version) < 170000:\n", + " actual_ver = await conn.fetchval(\"SHOW server_version;\")\n", + " print_step(\"❌\", f\"PostgreSQL version is {actual_ver}. HNSW Columnar Engine caching is only supported on PostgreSQL 17 or higher.\", is_error=True)\n", + " raise RuntimeError(\"Unsupported PostgreSQL Version.\")\n", + "\n", + " # 2. Fetch Columnar Engine GUC Settings\n", + " enabled = await conn.fetchval(\"SELECT current_setting('google_columnar_engine.enabled', true);\")\n", + " caching = await conn.fetchval(\"SELECT current_setting('google_columnar_engine.enable_index_caching', true);\")\n", + " mem_size = await conn.fetchval(\"SELECT current_setting('google_columnar_engine.memory_size_in_mb', true);\")\n", + "\n", + " warnings = []\n", + " if enabled != 'on':\n", + " warnings.append(\"google_columnar_engine.enabled is NOT 'on'\")\n", + " if caching != 'on':\n", + " warnings.append(\"google_columnar_engine.enable_index_caching is NOT 'on'\")\n", + " if not mem_size or int(mem_size) < 1024:\n", + " warnings.append(f\"memory_size_in_mb is '{mem_size}' (Must be at least 1024)\")\n", + "\n", + " if warnings:\n", + " for w in warnings:\n", + " print_step(\"❌\", w, is_error=True)\n", + " raise RuntimeError(\"Missing required Database Flags. Please update your cluster settings and restart.\")\n", + " else:\n", + " print_step(\"✅\", \"PostgreSQL version and Columnar Engine flags are properly configured!\", is_success=True)\n", + "\n", + "async def fast_bulk_insert_train(conn, data_array, batch_size=10000, desc=\"\"):\n", + " for i in tqdm(range(0, len(data_array), batch_size), desc=desc, leave=True, colour='#1f77b4'):\n", + " batch = data_array[i:i + batch_size]\n", + " records = [(i + j + 1, emb.tolist()) for j, emb in enumerate(batch)]\n", + " await conn.copy_records_to_table(\"glove\", columns=[\"id\", \"embedding\"], records=records)\n", + "\n", + "async def fast_bulk_insert_test(conn, data_array, neighbors_array, batch_size=10000, desc=\"\"):\n", + " for i in tqdm(range(0, len(data_array), batch_size), desc=desc, leave=True, colour='#1f77b4'):\n", + " batch_d = data_array[i:i + batch_size]\n", + " batch_n = neighbors_array[i:i + batch_size]\n", + " records = [(i + j + 1, emb.tolist(), [int(x+1) for x in nbr]) for j, (emb, nbr) in enumerate(zip(batch_d, batch_n))]\n", + " await conn.copy_records_to_table(\"glove_test\", columns=[\"id\", \"embedding\", \"ground_truth\"], records=records)\n", + "\n", + "async def prepare_dataset(conn):\n", + " print_step(\"Step 4/9:\", \"📥 Preparing GloVe Dataset...\")\n", + " if not os.path.exists(\"glove-100-angular.hdf5\"):\n", + " console.print(\" [dim]Downloading dataset (1.2GB) from ann-benchmarks.com...[/dim]\")\n", + " url = \"http://ann-benchmarks.com/glove-100-angular.hdf5\"\n", + " req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})\n", + " with urllib.request.urlopen(req) as response, open(\"glove-100-angular.hdf5\", \"wb\") as out_file:\n", + " shutil.copyfileobj(response, out_file)\n", + "\n", + " print_step(\"Step 5/9:\", \"📂 Loading dataset and inserting via COPY...\")\n", + " with h5py.File(\"glove-100-angular.hdf5\", \"r\") as f:\n", + " train_data = f['train'][:]\n", + " test_data = f['test'][:]\n", + " neighbors_data = f['neighbors'][:]\n", + "\n", + " await fast_bulk_insert_train(conn, train_data, desc=\"COPY Training Data\")\n", + " await fast_bulk_insert_test(conn, test_data, neighbors_data, desc=\"COPY Test Queries & Ground Truth\")\n", + "\n", + "async def monitor_index_progress(connector):\n", + " try:\n", + " poll_conn = await connector.connect(\n", + " f\"projects/{project_id}/locations/{region}/clusters/{cluster_name}/instances/{instance_name}\",\n", + " \"asyncpg\", user=db_user, password=db_pass, db=db_name, ip_type=\"PUBLIC\"\n", + " )\n", + " except Exception:\n", + " return\n", + "\n", + " try:\n", + " with tqdm(total=100, desc=\"Building HNSW Index\", colour='#00ff00') as pbar:\n", + " while True:\n", + " try:\n", + " progress = await poll_conn.fetchrow(\"\"\"\n", + " SELECT blocks_done, blocks_total, phase\n", + " FROM pg_stat_progress_create_index\n", + " WHERE relid = 'glove'::regclass\n", + " \"\"\")\n", + " if progress and progress['blocks_total'] > 0:\n", + " percent = (progress['blocks_done'] / progress['blocks_total']) * 100\n", + " pbar.n = round(percent, 1)\n", + " pbar.set_postfix_str(f\"Phase: {progress['phase']}\")\n", + " pbar.refresh()\n", + " except Exception:\n", + " pass\n", + " await asyncio.sleep(1)\n", + " except asyncio.CancelledError:\n", + " pass\n", + " finally:\n", + " try:\n", + " await poll_conn.close()\n", + " except Exception:\n", + " pass\n", + "\n", + "async def build_index(conn, connector):\n", + " print_step(\"Step 6/9:\", \"🧹 Updating database planner statistics...\")\n", + " await conn.execute(\"VACUUM (DISABLE_PAGE_SKIPPING, ANALYZE) glove;\")\n", + " await conn.execute(\"VACUUM (DISABLE_PAGE_SKIPPING, ANALYZE) glove_test;\")\n", + "\n", + " print_step(\"Step 7/9:\", \"🏗️ Building HNSW Index...\")\n", + " await conn.execute(\"DROP INDEX IF EXISTS my_hnsw_idx;\")\n", + " await conn.execute(\"SET max_parallel_maintenance_workers = 16;\")\n", + " await conn.execute(\"SET maintenance_work_mem = '3GB';\")\n", + "\n", + " monitor_task = asyncio.create_task(monitor_index_progress(connector))\n", + " try:\n", + " await conn.execute(\"CREATE INDEX my_hnsw_idx ON glove USING hnsw (embedding halfvec_cosine_ops) WITH (m = 24, ef_construction = 256);\")\n", + " finally:\n", + " monitor_task.cancel()\n", + "\n", + "async def run_evaluations(conn, ef_values, test_queries):\n", + " print_step(\"Step 8/9:\", \"⚙️ Deploying Benchmark PL/pgSQL Functions...\")\n", + " await conn.execute(SQL_MEASURE_RECALL)\n", + " await conn.execute(SQL_MEASURE_QPS)\n", + "\n", + " print_step(\"Step 9/9:\", \"⏱️ Running Benchmarks...\")\n", + " results_without_ce = []\n", + " results_with_ce = []\n", + "\n", + " # Run WITHOUT Columnar Engine\n", + " await conn.execute(\"SELECT google_columnar_engine_drop_index('my_hnsw_idx');\")\n", + " for ef in tqdm(ef_values, desc=\"Without Columnar Engine\", leave=True, colour='#1f77b4'):\n", + " recall = await conn.fetchval(f\"SELECT measure_recall({ef}, {test_queries});\")\n", + " qps = await conn.fetchval(f\"SELECT measure_qps({ef}, {test_queries});\")\n", + " console.log(\"ef: %d | qps: %d | recall: %.3f\" % (ef, qps, recall))\n", + " results_without_ce.append((ef, recall, qps))\n", + "\n", + " # Run WITH Columnar Engine\n", + " await conn.execute(\"SELECT google_columnar_engine_add_index('my_hnsw_idx');\")\n", + " for ef in tqdm(ef_values, desc=\"WITH Columnar Engine\", leave=True, colour='#ff7f0e'):\n", + " recall = await conn.fetchval(f\"SELECT measure_recall({ef}, {test_queries});\")\n", + " qps = await conn.fetchval(f\"SELECT measure_qps({ef}, {test_queries});\")\n", + " console.log(\"ef: %d | qps: %d | recall: %.3f\" % (ef, qps, recall))\n", + " results_with_ce.append((ef, recall, qps))\n", + "\n", + " return results_without_ce, results_with_ce\n", + "\n", + "def plot_results(results_without_ce, results_with_ce):\n", + " \"\"\"Renders the final performance plot outside the async block.\"\"\"\n", + " print_step(\"📊\", \"Generating Final Plot & Summary...\", is_success=True)\n", + " import numpy as np\n", + " import matplotlib.patheffects as patheffects\n", + "\n", + " ef_wo = [p[0] for p in results_without_ce]\n", + " recalls_wo = [p[1] for p in results_without_ce]\n", + " qps_wo = [p[2] for p in results_without_ce]\n", + "\n", + " ef_wi = [p[0] for p in results_with_ce]\n", + " recalls_wi = [p[1] for p in results_with_ce]\n", + " qps_wi = [p[2] for p in results_with_ce]\n", + "\n", + " plt.rc('font', size=14)\n", + " plt.rc('axes', titlesize=18)\n", + " plt.rc('axes', labelsize=16)\n", + " plt.rc('xtick', labelsize=14)\n", + " plt.rc('ytick', labelsize=14)\n", + " plt.rc('legend', fontsize=14)\n", + " plt.figure(figsize=(12, 7))\n", + "\n", + " # Google brand colors\n", + " color_wi = '#ff7f0e' # Orange\n", + " color_wo = '#1f77b4' # Blue\n", + " color_ar = '#2ca02c' # Green\n", + "\n", + " plt.plot(recalls_wi, qps_wi, marker='D', linewidth=3, markersize=10,\n", + " color=color_wi, label='With Columnar Engine')\n", + " plt.plot(recalls_wo, qps_wo, marker='o', linewidth=3, markersize=10,\n", + " color=color_wo, label='Without Columnar Engine')\n", + "\n", + " plt.xlabel('Recall (Accuracy)')\n", + " plt.ylabel('Queries Per Second (QPS)')\n", + " plt.title(f'HNSW Recall vs QPS\\n(GloVe 100, LIMIT {search_limit}, {test_queries} Test Queries)', pad=20, fontweight='bold')\n", + "\n", + " max_qps = max(max(qps_wi, default=0), max(qps_wo, default=0))\n", + " min_recall = min(min(recalls_wi, default=1), min(recalls_wo, default=1))\n", + "\n", + " plt.ylim(0, max_qps + 500)\n", + " plt.xlim(min_recall - 0.015, 1.015)\n", + "\n", + " plt.gca().yaxis.set_major_locator(ticker.MultipleLocator(1000))\n", + " plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(0.05))\n", + "\n", + " # Clean up spines (borders)\n", + " plt.gca().spines['top'].set_visible(False)\n", + " plt.gca().spines['right'].set_visible(False)\n", + " plt.gca().spines['left'].set_color('#cccccc')\n", + " plt.gca().spines['bottom'].set_color('#cccccc')\n", + "\n", + " pe = [patheffects.withStroke(linewidth=3, foreground='white', alpha=0.9)]\n", + "\n", + " # --- 1. Annotate QPS improvement at the same Recall (Vertical Gap) ---\n", + " for r_wi, q_wi, q_wo in zip(recalls_wi, qps_wi, qps_wo):\n", + " multiplier = q_wi / q_wo if q_wo > 0 else 0\n", + " if multiplier > 0:\n", + " plt.text(r_wi * 1.001, q_wi + (max_qps * 0.04), f'{multiplier:.1f}x QPS', color=color_wi,\n", + " fontsize=12, fontweight='bold', ha='center', va='bottom', path_effects=pe)\n", + "\n", + " # --- 2. Annotate Recall improvement at the same QPS (Horizontal Gap) ---\n", + " if len(qps_wo) > 0 and len(qps_wi) > 0:\n", + " idx_min_recall_wo = np.argmin(recalls_wo)\n", + " r_wo_min = recalls_wo[idx_min_recall_wo]\n", + " q_target = qps_wo[idx_min_recall_wo]\n", + "\n", + " # Check if q_target is within the range of orange line's QPS\n", + " if min(qps_wi) <= q_target <= max(qps_wi):\n", + " wi_sort = np.argsort(qps_wi)\n", + " r_wi_interp = np.interp(q_target, np.array(qps_wi)[wi_sort], np.array(recalls_wi)[wi_sort])\n", + "\n", + " if (r_wi_interp - r_wo_min) > 0.005:\n", + " plt.annotate('', xy=(r_wi_interp, q_target), xytext=(r_wo_min, q_target),\n", + " arrowprops=dict(arrowstyle=\"<->\", color=color_ar, lw=1.5, linestyle='--'))\n", + " plt.text((r_wo_min + r_wi_interp) / 2, q_target + (max_qps * 0.015), f'+{r_wi_interp - r_wo_min:.3f} Recall',\n", + " color=color_ar, fontsize=11, fontweight='bold', ha='center', va='bottom', path_effects=pe)\n", + "\n", + " plt.legend(loc='upper right', frameon=True, edgecolor='#cccccc')\n", + " plt.grid(True, which='both', color='#f0f0f0', linestyle='-', linewidth=1.5)\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "# ==========================================\n", + "# 6. MAIN EXECUTION PIPELINE\n", + "# ==========================================\n", + "async def run_benchmark():\n", + " connector = None\n", + " conn = None\n", + " try:\n", + " print_step(\"✅\", \"GCP Authentication Successful!\", is_success=True)\n", + " print_step(\"🚀\", \"Starting AlloyDB Benchmark Pipeline...\")\n", + "\n", + " print_step(\"Step 1/9:\", \"🔌 Initializing AlloyDB AsyncConnector...\")\n", + " connector = AsyncConnector()\n", + " conn = await connector.connect(\n", + " f\"projects/{project_id}/locations/{region}/clusters/{cluster_name}/instances/{instance_name}\",\n", + " \"asyncpg\", user=db_user, password=db_pass, db=db_name, ip_type=\"PUBLIC\"\n", + " )\n", + "\n", + " await verify_columnar_flags(conn)\n", + "\n", + " print_step(\"Step 3/9:\", \"🛠️ Setting up extensions & tables...\")\n", + " await conn.execute(\"CREATE EXTENSION IF NOT EXISTS vector;\")\n", + " await conn.execute(\"CREATE EXTENSION IF NOT EXISTS google_columnar_engine;\")\n", + " await conn.execute(\"DROP TABLE IF EXISTS glove CASCADE;\")\n", + " await conn.execute(\"DROP TABLE IF EXISTS glove_test CASCADE;\")\n", + " await conn.execute(f\"CREATE TABLE glove (id BIGINT PRIMARY KEY, embedding halfvec({vector_dim}));\")\n", + " await conn.execute(f\"CREATE TABLE glove_test (id BIGINT PRIMARY KEY, embedding halfvec({vector_dim}), ground_truth INT[]);\")\n", + "\n", + " await register_vector(conn)\n", + "\n", + " await prepare_dataset(conn)\n", + " await build_index(conn, connector)\n", + "\n", + " ef_values = [40, 100, 200, 400, 800]\n", + " # Return results to pass into synchronous plotter\n", + " return await run_evaluations(conn, ef_values, test_queries)\n", + "\n", + " except Exception as e:\n", + " print_step(\"❌\", f\"ERROR: {str(e)}\", is_error=True)\n", + " return None, None\n", + " finally:\n", + " if conn: await conn.close()\n", + " if connector: await connector.close()\n", + "\n", + "# Evaluate the benchmark (Async Loop)\n", + "results_wo, results_wi = await run_benchmark()\n", + "\n", + "# Guarantee Plot renders perfectly (Sync Thread)\n", + "if results_wo and results_wi:\n", + " plot_results(results_wo, results_wi)\n" + ], + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "432349805c034a9d8387761723cf4d98", + "0b6b474fb4254eeeb4b10c41a558d026", + "afeb5acb553247829d1685defe5aeae9", + "80f6a24f9771464fadfea752d15e6159", + "b9ad738dc0e34bafababa2f50198cc3f", + "886844e5c3874bb189a5fbaa7f2a5229", + "0e5fbdb2a07443bea423da0311336208", + "09a6549846c94f8c84b617d1fb4f2b36", + "d28d52d7f0064c4f90744aeee370178c", + "f505b487933b463e8d265e762ac0a2f5", + "cb0b446d2e714182a1139727eaabe0e0", + "99e6dd6a55d349189089adb4fba19d93", + "5ac8dc5ac89d48498c34783f54f6e015", + "d9ba93ad10d94d53bd553aa7ea086442", + "1684af72fc9f43aca062ca5d6e887c4f", + "321ee5525a0842eab301ba2160eacb98", + "df58cbded6604c6d9d211361ac3d954c", + "3afbe5ec242d40d4ba1a4b870ef5a28f", + "64714cc191e24aa792ef8f67f5e309d1", + "ae55c2ae81554304b7e7ab07e6270a49", + "6567d66f7b8646028c071ed2521214fc", + "7db2112070a34ae694bf77dddbd19445", + "16bfa6a8251d43c6a3bfcc81b58ef63c", + "6d75f5b8d7af4c11afa85c8d551b0ad5", + "4752ff638d8f4a84bbb25b95352e56d3", + "5fc3bcb3bd0d4f8ea4b86fbde7326524", + "9d5d9193a59640949fcc673bd12320d3", + "6c80cf14a0e348e591ca737a0a7c2151", + "0b93402697d9441894546f1f51deeeac", + "891f07f3262e4258a1840526cffd87fa", + "3a6551b211e8497b99b8c8344f6f82a9", + "7878a1e13d4e4a7fb757428ad0ae51f2", + "fc74f66bd108472e99672b26ca5ada5f", + "83f4ec83f64e4f6ab2df423576dc8380", + "55319a6f81db40fdaa8c1dfb9b50ae3b", + "c1245c9f4fff4d5193c1753c1229b2a8", + "67c43b89c1c4481cb98f0fc56329d2f8", + "e95fa1b1633a4d18a4c68eb17a7b8a6c", + "5c02112c72e44d97acc0c9782f6c9d23", + "53f192594d5f4906a0635d58d091f095", + "5d3814089e814d5ea1a99862954520e3", + "3dd29ebfe5714fd6ad5154f98866fb1a", + "93ca114aeab04c54a76f74e01a29ca10", + "ff1a9748abeb4448810078ffb4f7f37f", + "350e3d35ca9649f4b69d7567f84c14c6", + "bfae5992b79840d6af157be49e9fb15d", + "63c04be1719741c58b55116e67d65368", + "2b24e18b05f14f8d9d2510580879a04e", + "4038a659c0ae417bb0e1a28fd1929388", + "3c215c43d90c4bf69b5ffded9bf8e908", + "5a42349f3460417e81d91bb0538ec016", + "42cc4676b7dd4eaba2e0d141d89c9cb6", + "8f567ae69e4044a8872fcf0ceadd0e42", + "ba12c2a4fda044d194d306ab57472991", + "57003bc1a11f47288fa45e3edd895342" + ] + }, + "id": "KOs-ppVTCKTu", + "jupyter": { + "source_hidden": true + }, + "outputId": "3109c1bc-b61a-4cb9-ba4d-292cf13328c6", + "tags": [ + "hide-input" + ] + }, + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ Output cleared. Colab Setup successful!\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " ✅ GCP Authentication Successful!\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " 🚀\n", + " Starting AlloyDB Benchmark Pipeline...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 1/9:\n", + " 🔌 Initializing AlloyDB AsyncConnector...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 2/9:\n", + " 🔍 Verifying PostgreSQL Version & Columnar Engine Flags...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " ✅ PostgreSQL version and Columnar Engine flags are properly configured!\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 3/9:\n", + " 🛠️ Setting up extensions & tables...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 4/9:\n", + " 📥 Preparing GloVe Dataset...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + " \u001b[2mDownloading dataset \u001b[0m\u001b[1;2m(\u001b[0m\u001b[1;2;36m1.\u001b[0m\u001b[2m2GB\u001b[0m\u001b[1;2m)\u001b[0m\u001b[2m from ann-benchmarks.com\u001b[0m\u001b[2;33m...\u001b[0m\n" + ], + "text/html": [ + "
         Downloading dataset (1.2GB) from ann-benchmarks.com...\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 5/9:\n", + " 📂 Loading dataset and inserting via COPY...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "COPY Training Data: 0%| | 0/119 [00:00" + ], + "text/html": [ + "\n", + "
\n", + " Step 6/9:\n", + " 🧹 Updating database planner statistics...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 7/9:\n", + " 🏗️ Building HNSW Index...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Building HNSW Index: 0%| | 0/100 [00:00" + ], + "text/html": [ + "\n", + "
\n", + " Step 8/9:\n", + " ⚙️ Deploying Benchmark PL/pgSQL Functions...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " Step 9/9:\n", + " ⏱️ Running Benchmarks...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Without Columnar Engine: 0%| | 0/5 [00:00[15:59:36] ef: 40 | qps: 880 | recall: 0.794 300156691.py:266\n", + "\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m[15:59:41]\u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m100\u001b[0m | qps: \u001b[1;36m430\u001b[0m | recall: \u001b[1;36m0.879\u001b[0m \u001b]8;id=831761;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=441919;file:///tmp/ipykernel_618/300156691.py#266\u001b\\\u001b[2m266\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
[15:59:41] ef: 100 | qps: 430 | recall: 0.879                                                      300156691.py:266\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m[15:59:49]\u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m200\u001b[0m | qps: \u001b[1;36m240\u001b[0m | recall: \u001b[1;36m0.923\u001b[0m \u001b]8;id=86592;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=69064;file:///tmp/ipykernel_618/300156691.py#266\u001b\\\u001b[2m266\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
[15:59:49] ef: 200 | qps: 240 | recall: 0.923                                                      300156691.py:266\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m[16:00:05]\u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m400\u001b[0m | qps: \u001b[1;36m131\u001b[0m | recall: \u001b[1;36m0.957\u001b[0m \u001b]8;id=694220;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=664071;file:///tmp/ipykernel_618/300156691.py#266\u001b\\\u001b[2m266\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
[16:00:05] ef: 400 | qps: 131 | recall: 0.957                                                      300156691.py:266\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m[16:00:33]\u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m800\u001b[0m | qps: \u001b[1;36m71\u001b[0m | recall: \u001b[1;36m0.979\u001b[0m \u001b]8;id=976122;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=29520;file:///tmp/ipykernel_618/300156691.py#266\u001b\\\u001b[2m266\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
[16:00:33] ef: 800 | qps: 71 | recall: 0.979                                                       300156691.py:266\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "WITH Columnar Engine: 0%| | 0/5 [00:00[16:00:37] ef: 40 | qps: 6914 | recall: 0.794 300156691.py:274\n", + "\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m \u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m100\u001b[0m | qps: \u001b[1;36m3427\u001b[0m | recall: \u001b[1;36m0.879\u001b[0m \u001b]8;id=138631;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=994064;file:///tmp/ipykernel_618/300156691.py#274\u001b\\\u001b[2m274\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
           ef: 100 | qps: 3427 | recall: 0.879                                                     300156691.py:274\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m[16:00:39]\u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m200\u001b[0m | qps: \u001b[1;36m1891\u001b[0m | recall: \u001b[1;36m0.923\u001b[0m \u001b]8;id=1464;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=53611;file:///tmp/ipykernel_618/300156691.py#274\u001b\\\u001b[2m274\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
[16:00:39] ef: 200 | qps: 1891 | recall: 0.923                                                     300156691.py:274\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m[16:00:41]\u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m400\u001b[0m | qps: \u001b[1;36m1015\u001b[0m | recall: \u001b[1;36m0.957\u001b[0m \u001b]8;id=90936;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=124803;file:///tmp/ipykernel_618/300156691.py#274\u001b\\\u001b[2m274\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
[16:00:41] ef: 400 | qps: 1015 | recall: 0.957                                                     300156691.py:274\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "\u001b[2;36m[16:00:45]\u001b[0m\u001b[2;36m \u001b[0mef: \u001b[1;36m800\u001b[0m | qps: \u001b[1;36m520\u001b[0m | recall: \u001b[1;36m0.979\u001b[0m \u001b]8;id=350436;file:///tmp/ipykernel_618/300156691.py\u001b\\\u001b[2m300156691.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=417242;file:///tmp/ipykernel_618/300156691.py#274\u001b\\\u001b[2m274\u001b[0m\u001b]8;;\u001b\\\n" + ], + "text/html": [ + "
[16:00:45] ef: 800 | qps: 520 | recall: 0.979                                                      300156691.py:274\n",
+              "
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "
\n", + " 📊 Generating Final Plot & Summary...\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAABJkAAAKlCAYAAABooj3HAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4FNXbxvHvpgfS6F0C0kEIvUlCryIiAoJKR0AQ+KmIqEhUqoJSLYiCAgIiXaUIJqH3XgWkFyFAQidt3j/2zZrNbippwP25rr3YPefMmWcmO0v2yTlnTIZhGIiIiIiIiIiIiDwEh8wOQEREREREREREHn1KMomIiIiIiIiIyENTkklERERERERERB6akkwiIiIiIiIiIvLQlGQSEREREREREZGHpiSTiIiIiIiIiIg8NCWZRERERERERETkoSnJJCIiIiIiIiIiD01JJhEREREREREReWhKMomIiIhIsnTr1g2TyWR51K9f36ZN3HqTycSsWbMyPE4RERHJHEoyiYiIZGH169e3+dKekNOnT9u07datm007X19fm3YvvPBCgv3GbxsYGJhg2xUrVtC5c2dKlSqFp6cnLi4u5M2blzJlylC/fn3efPNNvv/+e44dO2a13cyZM232c/bsWbv7KFu2rFW7fPny2W138+ZNnJycrNpOmTIlwdjtiZ9UiftwcHDAw8ODokWL0rx5c7744gvCwsJS1L88HlauXMkbb7xBpUqVyJs3Ly4uLvj4+FCiRAk6duzI9OnTuXPnTqJ9BAYGJvheM5lMODk5kSNHDqpUqcKAAQPYvn17ov3dvXuXb7/9lpYtW1K4cGHc3d1xd3enUKFCPPPMM7Rs2ZJhw4axYMECrly5kpanQ0REnmBOmR2AiIiIZL5ly5axdetWatWqlartb968yYsvvsi6dets6q5evcrVq1c5duwYISEhALRr145ff/3V0qZevXo2261fv55XX33Vpq+jR49alV25coVjx45RunRpq/JNmzYRHR1tVebv75+yA0uEYRjcuXOHO3fucPbsWVavXs3o0aNZtmwZdevWTbP9SNa1e/duunfvzv79+23qwsPDCQ8P5+TJk/zyyy8MGzaMUaNG0bdv31TtKzo6mrCwMPbs2cOePXuYNm0a3bp1Y/r06Tg7O1u1PXjwIM8//zynTp2y6efixYtcvHiRgwcPsnLlSgCmTJnCgAEDUhWXiIhIXBrJJCIiIgC89957qd72tddes5tgSq4SJUpQoEABq7L169fbtNu4caPd7Tds2GBTFn97b29vnnnmmVTHmBzXrl2jffv2SY5akUffkiVLqFOnjt0Ekz3Xr1+nX79+qU4y2TNr1iz69+9vVXbr1i2aN29uN8EkIiKS3pRkEhEREQBCQkIsIxtS4sCBAyxfvtyqrG3btvzxxx8cPHiQo0ePsn79eqZNm0a7du3w9PS020/80Uz2kkz2kkkJlcffvm7dujg4PPyvPhs2bODUqVMcOHCAH374gdy5c1vVX7p0idWrVz/0fiTr2rt3L6+++ioPHjywKu/QoQOrV6/m2LFjbN68mSFDhuDi4mLV5ttvv2XixInJ2s+8efM4deoUp06dYsuWLQwePNimzYwZM6wSSrNmzeLChQuW1w4ODrz77rusX7+eY8eOceDAAVatWsWYMWNo2LAhTk6a2CAiImlH/6uIiIiIxfvvv0/z5s0TXfspvvgJnuLFi7No0SKrPkqXLk29evV44403uHv3LocOHbLpx9/fn19++cXy+tixY1y5coW8efPa3VfhwoU5f/683Rju37/Pzp07rcrsTclLjcKFC+Pr6wtAhQoVuH37NgMHDrRqc+LEiQS3j46OZtGiRfz666/s2LGDK1euEBMTQ/78+alduzbdu3enSZMmicZgGAarVq1i/vz5bN++nUuXLnH37l1y585NkSJFqF+/Pq+88goVK1a0bHP69GmWL1/Onj17OHjwIFevXuX69evcu3cPT09PfH19qVWrFt27d6d69eqpP0EPYd++ffj5+VmV7d+/3+4ItJkzZ9KjRw/La09PTy5dukT27NkB8/vn66+/Zv369Zw6dYrbt2+TLVs28uTJQ+HChalWrRo1atSgVatWlm2S66233uLu3btWZSNHjuSDDz6wKqtduzbNmjWjWbNmVlM3R4wYQZcuXciZM2ei+8mfP7/lvRb78wkNDWXOnDmWNoZhsG7dOnr16gXYXgtdunRh3LhxVmUVKlSgWbNmvPfee1y6dIlbt24l78BFRESSoJFMIiIiTzg3NzfL87179zJv3rwUbR9/seuIiAibER5xZcuWzW4Sw14SKO4X5tu3b7Nnzx7L6yFDhlienzp1ypJwAti6dSsRERFJ9p8WSpQoYVOW0GitY8eOUaVKFTp2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3r17KVasWNqfmDRgL5nk6elJ+/bt7bbPnj0727dv58svv+Sll16iYsWKFChQAGdnZ1xdXSlUqBBNmzZl0qRJHDp0KMHRUMlRs2ZNDhw4wB9//EG/fv3w8/MjT548NklXBwcHVq5cmWDCLC2NHj2aU6dOMX36dHr06EHNmjXJnz8/7u7uODo64unpSZkyZejcuTOLFy9m06ZN6T7aT0REnhwmI7PuTSsiIiIi8pgaP348Q4YMsbwuXrw4W7ZsUUJHREQea0oyiYiIiIikg169evH9999bXteoUYOgoCCyZcuWiVGJiIikHyWZRERERETSQWRkJM2aNSMoKMhS9vzzz7NkyRIcHLRqhYiIPH6UZBIRERERSSfXr19nypQpxP2V+8UXX6RixYqZGJWIiEj6UJJJREREREREREQemsbpioiIiIiIiIjIQ1OSSUREREREREREHpqSTCIiIiIiIiIi8tCUZBIRERERERERkYemJJOISAYaNmwYJpMJk8mEk5MTx48fT/d9+vr6WvZpMpkIDAxM932KiIhI8gUHB1v9X20ymTh9+nRmh2XF39/fEluxYsV48OBBZockIlmQkkwiIhnk3LlzTJw40fK6Q4cOlCxZMtFtIiIimDdvHr1796ZSpUrky5cPFxcX3N3dyZs3L9WrV6dLly589913XLp0KZ2PwD4/Pz+rX4oLFy5MTExMkttVqVLFarsCBQoQFRWVARGbXblyhV9//ZWhQ4fSsGFDvL29bX7BDw4OTnZ/f/zxB507d8bX1xd3d3e8vb0pV64cb775JgcPHkxWH6GhoXz88cfUrFmT3Llz4+rqSsGCBWnevDnTp08nMjIylUebfIGBgQ91Hrp165bkF6VZs2bZtDGZTJQpU4bEbno7d+5cu9v5+vqmKA57x5jSR0rOCcDmzZuZPHkyr732GmXKlMHBwcGqv/r16ye7r6zwPgHYu3cv33zzDT179qRixYo4OTkl+XNJyO3bt5kwYQIBAQGWz7l8+fIREBDAhAkTuHPnTrL62bRpEz179qRkyZJ4eHjg4eFByZIl6dWrF5s3b87QWOKLn/BP6SMl5zOthYWFERgYaPVI6wTIpk2beOutt6hevTr58+fH1dUVLy8vihUrRps2bZg4cSLXrl1L031KynzwwQeW56dPn2bKlCmZGI2IZFmGiIhkiNdee80ADMAwmUzG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fLnGV6CvnJwcd9tgQSaHwyH+9re/iW7dugmtViuSkpLEpEmTgmYH8r4ok4QQos7m4hERERERERERUZPAwt9ERERERERERFRjDDIREREREREREVGNMchEREREREREREQ1xiATERERERERERHVGINMRERERERERERUYwwyERERERERERFRjTHIRERERERERERENcYgExERERERERER1RiDTEREREREREREVGMMMhEREVGdaNu2LSRJ8vrSarVIS0vDmDFjsGbNmnB3sVpcr6WiIUOGQJIkbN26tVrnHTduHHQ6Hc6dOxe03R133OHuw+HDh6v1XFQzp06dgkajwV133RXurhAREUUUBpmIiIioTg0cOBA5OTnIycnBqFGjoFKp8J///Ae33347Zs2aFe7uRYQvvvgCq1atwsMPP4y0tLSA7S5cuIC1a9e6v//Xv/5VH92jCtq1a4cHHngAn3zyCbZt2xbu7hAREUUMBpmIiIioTs2YMQMLFy7EwoULsXLlSvz44494+OGHAQB//etfsXfv3jD3MPwef/xxREVF4amnngrabtGiRXA4HGjVqhUAYMmSJbBarfXRRarg2WefhVqtxuOPPx7urhAREUUMBpmIiIioXqlUKrz66quIjY0FAHz22Wdh7lF4bdq0CYcPH8bYsWORlJQUtO37778PAHjttdeQkZGBq1evYvXq1fXRTaogNTUVo0aNwoEDB/Df//433N0hIiKKCAwyERERUb2LiopChw4dAACXLl3y2+bLL7/E+PHj0aJFC2g0GjRr1gzjxo3Drl27Ap7XZDLhjTfewKBBg5CQkACtVov09HTcfvvt+PDDD73a5uXl4eWXX8Ytt9yCNm3aQKvVIj4+HoMGDcK7774Lp9NZey84iLfeegsAMG3atKDttm3bhhMnTiApKQnjxo3D9OnTAVQ+Za4q7wkACCGwYsUKjB49GqmpqdBoNEhNTcWgQYPw8ssvw2w2u9tOmzYNkiRh4cKFfp974cKFkCTJ57V5bs/Pz8djjz2G9u3bQ6vVYsiQIe52X3zxBR555BH07t0bycnJ7ppekyZNqjQDbt++fcjJyUG7du0QFRWFxMRE9OrVC3/84x+Rl5cHAMjNzYUkScjOzg54nvPnz0OtVkOn0+HatWte+1yv6x//+EfQvhARETUVDDIRERFRWBQWFgIAmjdv7rPvD3/4A4YPH47Vq1ejTZs2GDt2LDIyMrB69WrcfPPNyM3N9Tnm7Nmz6NevHx5//HEcOHAA/fr1w/jx45Geno6vvvoKs2fP9mq/ePFiPPXUUzh9+jQ6duyI8ePHo3fv3ti7dy8eeugh3HnnnRBC1M2LL1NaWooNGzZArVZj8ODBQdu6gklTpkyBRqPBtGnToFAosGnTJpw9e9bvMVV9T2w2GyZOnIgJEyZg3bp1aNeuHSZOnIiePXvi9OnTeOqppwIGBavj6tWr6Nu3L/7973+je/fuGDNmjFdNqoceegjz58+HQqHAwIEDMXr0aMTFxWHZsmUYMGAAli9f7ve8r776Km666Sb8+9//hkajwZgxYzBo0CDYbDbMmzcPW7ZsAQDcc889SElJwaZNm3D8+HG/53r33Xdht9sxefJkn0yzW265BQqFAp9//jlsNlstvStEREQNmCAiIiKqA+np6QKAyM3N9dl35MgRoVQqBQCxd+9er33z588XAMQNN9wgDh065LVv27ZtIiYmRmg0GnH8+HH3dofDIfr27SsAiBEjRojLly97HWc2m8Xnn3/utW3Pnj3iu+++8+nbzz//LHr16iUAiGXLlvnsByD8fYTKysoSAMSWLVt89gXyxRdfCACiX79+QdsVFBQInU4nAIiDBw+6t2dnZwsA4i9/+YvPMdV5T2bNmiUAiLZt23o9jxBCOJ1O8cUXX4iCggL3tpycnIA/YyGEyM3NFQBETk6O3+0AxLBhw4TRaPR7/MqVK0V+fr7f7SqVSiQlJQmTyeS1b/Xq1QKAiIqKEkuXLvU59vvvvxdHjhxxf//MM88IAOLRRx/1aWu1WkVqaqoAIPbt2+e3jz179hQAxFdffeV3PxERUVPCTCYiIiKqN0ajERs3bsT48ePhcDjw7LPPom/fvu79TqcTzz//PADg448/Rs+ePb2OHzx4MP70pz/BarXi3XffdW//7LPP8M0336BFixZYvnw5UlJSvI6LiorCqFGjvLb169cP3bt39+ljy5Yt8corrwAAPvnkkxq93socOHAAANClS5eg7T766COYzWZkZmaiV69e7u333XcfAHnal6iQdVXV9+Ty5cvuqXuffvqp1/MAgCRJGDZsGOLi4qr4KgNTq9WYP3++uz5XRWPHjkVCQoLf7XfeeSeuXbvmzkpymTNnDgDghRdewF133eVzbNeuXb3e79/+9rdQq9VYtGgRSkpKvNouX74cFy9eRP/+/dGnTx+/fezWrRsAYP/+/UFeKRERUdOgCncHiIiIqHGbPn26u36Qi1KpxJIlSzBlyhSv7QcOHMD58+fRvn17ZGZm+j2fq2bPzp073dvWr18PQJ7+FB0dHXLfLBYLNm7ciL179+Ly5cuwWCwQQqCoqAgAcOzYsZDPVR2uqWeVFfxesGABAOA3v/mN1/YxY8YgKSkJp06dwubNmzFs2DD3vqq+J1u2bIHVakVmZmbA97623XjjjcjIyAja5vz58/j888/xww8/wGg0wm63AwC+//57APLPyBUsu3jxIg4ePAiFQuEOwFWmZcuWmDhxIj766CMsXrwYDz30kHufq9aSazVEf1w/u9qcRkhERNRQMchEREREdWrgwIG44YYbAABXrlzBV199haKiIsycORMdOnTATTfd5G578uRJAMBPP/0ESZKCnvfKlSvux65Czp07dw65X7t378akSZNw5syZgG1cdaPqitFoBICAmTwAcOjQIezbtw9RUVG45557vPZpNBpMmTIFf//73/H+++97BZmq+p5U5z2sqbZt2wbd/+c//xkvvPBC0HpHnj8j18+yRYsWVcq4evTRR/HRRx/hH//4hzvI9O2332L79u1o3rw5Jk6cGPBY18/u+vXrIT8fERFRY8UgExEREdWpGTNmeK0uZjQaMW7cOGzZsgV33XUXjhw5Ar1eDwDuFd1SU1ODrvgFAMnJydXuk8lkwtixY3Hp0iVMnz4dM2fOxA033IDY2FgolUocP34cnTp1qvPC3/Hx8QCCB7NcBb9VKhVGjx7ts9+14tmKFStQUFDgPmckqGyFPp1OF3DfihUr8PzzzyM6OhpvvfUWbrnlFrRs2RI6nQ6SJGH27Nl46aWXauVn9Mtf/hI33XQT9uzZg23btiErK8udxfTAAw9Ao9EEPNYVKPQ3rY+IiKipYZCJiIiI6lVcXByWLl2Kzp07Iy8vD6+//jqeffZZAEDr1q0ByFOQFi5cGPI527RpAwD44YcfQmr/3//+F5cuXUKfPn3w/vvv++w/ceJEyM9dE82aNQNQHiiqyGKx4IMPPgAAFBcXY8eOHQHPVVpaig8++AC/+93vAFT9PalqewDu4ItremFFruyo6li2bBkAubbSAw884LPf38/I9RouXLgAo9FY5WymqVOn4q23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\n" + }, + "metadata": {} + } + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "version_major": 2, + "version_minor": 0, + "state": { + "432349805c034a9d8387761723cf4d98": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_0b6b474fb4254eeeb4b10c41a558d026", + "IPY_MODEL_afeb5acb553247829d1685defe5aeae9", + "IPY_MODEL_80f6a24f9771464fadfea752d15e6159" + ], + "layout": "IPY_MODEL_b9ad738dc0e34bafababa2f50198cc3f" + } + }, + "0b6b474fb4254eeeb4b10c41a558d026": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_886844e5c3874bb189a5fbaa7f2a5229", + "placeholder": "​", + "style": "IPY_MODEL_0e5fbdb2a07443bea423da0311336208", + "value": "COPY Training Data: 100%" + } + }, + "afeb5acb553247829d1685defe5aeae9": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_09a6549846c94f8c84b617d1fb4f2b36", + "max": 119, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_d28d52d7f0064c4f90744aeee370178c", + "value": 119 + } + }, + "80f6a24f9771464fadfea752d15e6159": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_f505b487933b463e8d265e762ac0a2f5", + "placeholder": "​", + "style": "IPY_MODEL_cb0b446d2e714182a1139727eaabe0e0", + "value": " 119/119 [00:43<00:00,  2.78it/s]" + } + }, + "b9ad738dc0e34bafababa2f50198cc3f": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "886844e5c3874bb189a5fbaa7f2a5229": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0e5fbdb2a07443bea423da0311336208": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "09a6549846c94f8c84b617d1fb4f2b36": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d28d52d7f0064c4f90744aeee370178c": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#1f77b4", + "description_width": "" + } + }, + "f505b487933b463e8d265e762ac0a2f5": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "cb0b446d2e714182a1139727eaabe0e0": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "99e6dd6a55d349189089adb4fba19d93": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_5ac8dc5ac89d48498c34783f54f6e015", + "IPY_MODEL_d9ba93ad10d94d53bd553aa7ea086442", + "IPY_MODEL_1684af72fc9f43aca062ca5d6e887c4f" + ], + "layout": "IPY_MODEL_321ee5525a0842eab301ba2160eacb98" + } + }, + "5ac8dc5ac89d48498c34783f54f6e015": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_df58cbded6604c6d9d211361ac3d954c", + "placeholder": "​", + "style": "IPY_MODEL_3afbe5ec242d40d4ba1a4b870ef5a28f", + "value": "COPY Test Queries & Ground Truth: 100%" + } + }, + "d9ba93ad10d94d53bd553aa7ea086442": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_64714cc191e24aa792ef8f67f5e309d1", + "max": 1, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_ae55c2ae81554304b7e7ab07e6270a49", + "value": 1 + } + }, + "1684af72fc9f43aca062ca5d6e887c4f": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_6567d66f7b8646028c071ed2521214fc", + "placeholder": "​", + "style": "IPY_MODEL_7db2112070a34ae694bf77dddbd19445", + "value": " 1/1 [00:01<00:00,  1.30s/it]" + } + }, + "321ee5525a0842eab301ba2160eacb98": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "df58cbded6604c6d9d211361ac3d954c": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3afbe5ec242d40d4ba1a4b870ef5a28f": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "64714cc191e24aa792ef8f67f5e309d1": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ae55c2ae81554304b7e7ab07e6270a49": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#1f77b4", + "description_width": "" + } + }, + "6567d66f7b8646028c071ed2521214fc": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7db2112070a34ae694bf77dddbd19445": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "16bfa6a8251d43c6a3bfcc81b58ef63c": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_6d75f5b8d7af4c11afa85c8d551b0ad5", + "IPY_MODEL_4752ff638d8f4a84bbb25b95352e56d3", + "IPY_MODEL_5fc3bcb3bd0d4f8ea4b86fbde7326524" + ], + "layout": "IPY_MODEL_9d5d9193a59640949fcc673bd12320d3" + } + }, + "6d75f5b8d7af4c11afa85c8d551b0ad5": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_6c80cf14a0e348e591ca737a0a7c2151", + "placeholder": "​", + "style": "IPY_MODEL_0b93402697d9441894546f1f51deeeac", + "value": "Building HNSW Index: 100%" + } + }, + "4752ff638d8f4a84bbb25b95352e56d3": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_891f07f3262e4258a1840526cffd87fa", + "max": 100, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_3a6551b211e8497b99b8c8344f6f82a9", + "value": 100 + } + }, + "5fc3bcb3bd0d4f8ea4b86fbde7326524": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7878a1e13d4e4a7fb757428ad0ae51f2", + "placeholder": "​", + "style": "IPY_MODEL_fc74f66bd108472e99672b26ca5ada5f", + "value": " 100.0/100 [02:23<00:00,  1.44s/it, Phase: building index: loading tuples]" + } + }, + "9d5d9193a59640949fcc673bd12320d3": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6c80cf14a0e348e591ca737a0a7c2151": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0b93402697d9441894546f1f51deeeac": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "891f07f3262e4258a1840526cffd87fa": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3a6551b211e8497b99b8c8344f6f82a9": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#00ff00", + "description_width": "" + } + }, + "7878a1e13d4e4a7fb757428ad0ae51f2": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fc74f66bd108472e99672b26ca5ada5f": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "83f4ec83f64e4f6ab2df423576dc8380": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_55319a6f81db40fdaa8c1dfb9b50ae3b", + "IPY_MODEL_c1245c9f4fff4d5193c1753c1229b2a8", + "IPY_MODEL_67c43b89c1c4481cb98f0fc56329d2f8" + ], + "layout": "IPY_MODEL_e95fa1b1633a4d18a4c68eb17a7b8a6c" + } + }, + "55319a6f81db40fdaa8c1dfb9b50ae3b": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5c02112c72e44d97acc0c9782f6c9d23", + "placeholder": "​", + "style": "IPY_MODEL_53f192594d5f4906a0635d58d091f095", + "value": "Without Columnar Engine: 100%" + } + }, + "c1245c9f4fff4d5193c1753c1229b2a8": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_5d3814089e814d5ea1a99862954520e3", + "max": 5, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_3dd29ebfe5714fd6ad5154f98866fb1a", + "value": 5 + } + }, + "67c43b89c1c4481cb98f0fc56329d2f8": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_93ca114aeab04c54a76f74e01a29ca10", + "placeholder": "​", + "style": "IPY_MODEL_ff1a9748abeb4448810078ffb4f7f37f", + "value": " 5/5 [00:59<00:00, 16.46s/it]" + } + }, + "e95fa1b1633a4d18a4c68eb17a7b8a6c": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5c02112c72e44d97acc0c9782f6c9d23": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "53f192594d5f4906a0635d58d091f095": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "5d3814089e814d5ea1a99862954520e3": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3dd29ebfe5714fd6ad5154f98866fb1a": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#1f77b4", + "description_width": "" + } + }, + "93ca114aeab04c54a76f74e01a29ca10": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ff1a9748abeb4448810078ffb4f7f37f": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "350e3d35ca9649f4b69d7567f84c14c6": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HBoxModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_bfae5992b79840d6af157be49e9fb15d", + "IPY_MODEL_63c04be1719741c58b55116e67d65368", + "IPY_MODEL_2b24e18b05f14f8d9d2510580879a04e" + ], + "layout": "IPY_MODEL_4038a659c0ae417bb0e1a28fd1929388" + } + }, + "bfae5992b79840d6af157be49e9fb15d": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_3c215c43d90c4bf69b5ffded9bf8e908", + "placeholder": "​", + "style": "IPY_MODEL_5a42349f3460417e81d91bb0538ec016", + "value": "WITH Columnar Engine: 100%" + } + }, + "63c04be1719741c58b55116e67d65368": { + "model_module": "@jupyter-widgets/controls", + "model_name": "FloatProgressModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_42cc4676b7dd4eaba2e0d141d89c9cb6", + "max": 5, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_8f567ae69e4044a8872fcf0ceadd0e42", + "value": 5 + } + }, + "2b24e18b05f14f8d9d2510580879a04e": { + "model_module": "@jupyter-widgets/controls", + "model_name": "HTMLModel", + "model_module_version": "1.5.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_ba12c2a4fda044d194d306ab57472991", + "placeholder": "​", + "style": "IPY_MODEL_57003bc1a11f47288fa45e3edd895342", + "value": " 5/5 [00:08<00:00,  2.31s/it]" + } + }, + "4038a659c0ae417bb0e1a28fd1929388": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3c215c43d90c4bf69b5ffded9bf8e908": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5a42349f3460417e81d91bb0538ec016": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "42cc4676b7dd4eaba2e0d141d89c9cb6": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8f567ae69e4044a8872fcf0ceadd0e42": { + "model_module": "@jupyter-widgets/controls", + "model_name": "ProgressStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": "#ff7f0e", + "description_width": "" + } + }, + "ba12c2a4fda044d194d306ab57472991": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "57003bc1a11f47288fa45e3edd895342": { + "model_module": "@jupyter-widgets/controls", + "model_name": "DescriptionStyleModel", + "model_module_version": "1.5.0", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + } + } + } + } + }, + "nbformat_minor": 0, + "nbformat": 4 +} diff --git a/alloydb/notebooks/e2e_test.py b/alloydb/notebooks/e2e_test.py index 34516188c4a..36a122fd6f4 100644 --- a/alloydb/notebooks/e2e_test.py +++ b/alloydb/notebooks/e2e_test.py @@ -153,3 +153,43 @@ async def test_embeddings_batch_processing( ) await conn.commit() await pool.dispose() + + +@pytest.mark.asyncio +async def test_alloydb_vector_search_benchmark( + project_id: str, + cluster_name: str, + instance_name: str, + region: str, + database_name: str, + password: str, +) -> None: + # Run the benchmark notebook with significantly reduced parameters + # to allow CI to complete quickly without OOMs or timeouts + conftest.run_notebook( + "alloydb_vector_search_benchmark.ipynb", + variables={ + "project_id": project_id, + "region": region, + "cluster_id": cluster_name, + "instance_id": instance_name, + "db_name": database_name, + "db_user": "postgres", + "test_queries": 10, + }, + preprocess=preprocess, + skip_shell_commands=True, + replace={ + ( + "db_pass = getpass.getpass(" + '"🔑 Enter Database Password for AlloyDB: ")' + ): f"db_pass = '{password}'", + "train_data = f['train'][:]": "train_data = f['train'][:1000]", + "test_data = f['test'][:]": "test_data = f['test'][:100]", + "neighbors_data = f['neighbors'][:]": ( + "neighbors_data = f['neighbors'][:100]" + ), + "ef_values = [40, 100, 200, 400, 800]": "ef_values = [40]", + }, + until_end=True, + ) diff --git a/alloydb/notebooks/noxfile_config.py b/alloydb/notebooks/noxfile_config.py index f5bc1ea9e2f..7f53f031cd7 100644 --- a/alloydb/notebooks/noxfile_config.py +++ b/alloydb/notebooks/noxfile_config.py @@ -14,7 +14,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/alloydb/notebooks/requirements-test.txt b/alloydb/notebooks/requirements-test.txt index ba12393197f..c6a5cfc35b3 100644 --- a/alloydb/notebooks/requirements-test.txt +++ b/alloydb/notebooks/requirements-test.txt @@ -1,6 +1,6 @@ -google-cloud-alloydb-connector[asyncpg]==1.5.0 +google-cloud-alloydb-connector[asyncpg]==1.12.1 sqlalchemy==2.0.40 -pytest==8.3.3 +pytest==9.0.3 ipykernel==6.29.5 -pytest-asyncio==0.24.0 -nbconvert==7.16.6 \ No newline at end of file +pytest-asyncio==1.3.0 +nbconvert==7.17.1 \ No newline at end of file diff --git a/alm/ui-examples/Dockerfile b/alm/ui-examples/Dockerfile new file mode 100644 index 00000000000..cf18b2e0362 --- /dev/null +++ b/alm/ui-examples/Dockerfile @@ -0,0 +1,49 @@ +# 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. + +# Use a slim version to keep the image small +FROM python:3.12-slim + +# Set working directory +WORKDIR /app + +# Prevent Python from writing .pyc files and enable unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Install system dependencies +# - 'curl' is required to download the gcloud SDK installer +# - 'build-essential' is kept in case any Python packages need compiling +# Note: We dropped 'jq' because Python's requests library handles JSON parsing now! +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + build-essential \ + && curl -sSL https://sdk.cloud.google.com | bash -s -- --disable-prompts \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Set up the Cloud SDK path so Python's subprocess can find 'gcloud' +ENV PATH="/root/google-cloud-sdk/bin:${PATH}" + +# Install Python packages +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy app code into container +COPY . . + +# Cloud Run sets the $PORT environment variable automatically +# We use exec form for the CMD to handle signals properly +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app + diff --git a/alm/ui-examples/README.md b/alm/ui-examples/README.md new file mode 100644 index 00000000000..9bc0baae92a --- /dev/null +++ b/alm/ui-examples/README.md @@ -0,0 +1,52 @@ + + +# ALM UI Samples + +This directory contains a sample Flask application intended to be run under App Lifecycle Management (ALM). + +## Setup + +First, activate your virtual environment and run the application locally: + +```bash +source .venv/bin/activate +python app.py +``` + +## Deployment + +To deploy this containerized application to Artifact Registry, execute the following commands. +Ensure that you replace `your-project-id` and `your-region` with your actual Google Cloud Project ID and region! + +```bash +export _REGION="your-region" +export _PREFIX="region-deployment" +export _projectID="your-project-id" +export _version="v2.01.0" + +# Create the Artifact Registry repository +gcloud artifacts repositories create ${_PREFIX} \ + --repository-format=docker \ + --location=$_REGION \ + --project=${_projectID} + +# Build the docker container +docker build --tag ${_REGION}-docker.pkg.dev/${_projectID}/${_PREFIX}/${_version}:latest . + +# Push the docker container to Artifact Registry +docker push ${_REGION}-docker.pkg.dev/${_projectID}/${_PREFIX}/${_version}:latest +``` diff --git a/alm/ui-examples/app.py b/alm/ui-examples/app.py new file mode 100644 index 00000000000..5ce8bac3844 --- /dev/null +++ b/alm/ui-examples/app.py @@ -0,0 +1,223 @@ +# 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 alm_ui_sample] +import os +import subprocess + +from flask import Flask, make_response, render_template, request +import requests + +app = Flask(__name__) + +GCP_REGIONS = [ + "africa-south1", + "asia-east1", + "asia-east2", + "asia-northeast1", + "asia-northeast2", + "asia-northeast3", + "asia-south1", + "asia-south2", + "asia-southeast1", + "asia-southeast2", + "australia-southeast1", + "australia-southeast2", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west2", + "europe-west3", + "europe-west4", + "europe-west6", + "europe-west8", + "europe-west9", + "europe-west10", + "europe-west12", + "me-central1", + "me-central2", + "me-west1", + "northamerica-northeast1", + "northamerica-northeast2", + "southamerica-east1", + "southamerica-west1", + "us-central1", + "us-east1", + "us-east4", + "us-east5", + "us-south1", + "us-west1", + "us-west2", + "us-west3", + "us-west4", +] + + +# --- PAGE 1: Apigee Generate & Deploy --- +@app.route("/", methods=["GET", "POST"]) +def generate_api(): + action_result = None + error_message = None + + if request.method == "POST": + # Get dynamic variables from the form + apigee_org = request.form.get("apigee_org") + revision = "12" # request.form.get("revision") + service_account = request.form.get("service_account") + + # Hardcoded variables + apigee_env = "prod" + apigee_api = "saas-runtime" + zip_filename = f"{apigee_api}_rev{revision}.zip" + + try: + # --- TOKEN GENERATION --- + print("Generating gcloud token...") + token_cmd = [ + "gcloud", + "auth", + "print-access-token", + f"--impersonate-service-account={service_account}", + ] + token = subprocess.check_output(token_cmd, text=True).strip() + + headers = {"Authorization": f"Bearer {token}"} + + # --- STEP 1: Download --- + print(f"Step 1: Downloading {apigee_api} revision {revision}...") + download_url = ( + f"https://apigee.googleapis.com/v1/organizations/{apigee_org}/" + f"apis/{apigee_api}/revisions/{revision}?format=bundle" + ) + dl_response = requests.get(download_url, headers=headers) + dl_response.raise_for_status() + + # Save the zip file locally + with open(zip_filename, "wb") as f: + f.write(dl_response.content) + + # --- STEP 2: Import --- + print("Step 2: Importing proxy bundle...") + import_url = ( + f"https://apigee.googleapis.com/v1/organizations/{apigee_org}/" + f"apis?name={apigee_api}&action=import" + ) + + # Open the file and send as multipart/form-data + with open(zip_filename, "rb") as f: + files = {"file": (zip_filename, f, "application/zip")} + import_response = requests.post( + import_url, headers=headers, files=files + ) + + import_response.raise_for_status() + new_revision = import_response.json().get("revision") + print(f"Imported successfully! New Apigee Revision: {new_revision}") + + # --- STEP 3: Deploy --- + print(f"Step 3: Deploying revision {new_revision} to {apigee_env}...") + deploy_url = ( + f"https://apigee.googleapis.com/v1/organizations/{apigee_org}/" + f"environments/{apigee_env}/apis/{apigee_api}/revisions/{new_revision}/" + f"deployments?serviceAccount={service_account}&override=true" + ) + deploy_response = requests.post(deploy_url, headers=headers) + deploy_response.raise_for_status() + + # Clean up the local zip file + if os.path.exists(zip_filename): + os.remove(zip_filename) + + action_result = ( + f"Success! Revision {new_revision} of {apigee_api} " + f"deployed to {apigee_env}." + ) + + # Set a cookie just in case Page 2 needs to know what revision was created + resp = make_response( + render_template("page1.html", action_result=action_result) + ) + resp.set_cookie("last_deployed_revision", str(new_revision)) + return resp + + except subprocess.CalledProcessError: + error_message = ( + "Gcloud Token Error: Failed to impersonate service account. " + "Make sure gcloud is authenticated." + ) + except requests.exceptions.RequestException as e: + error_message = f"API Error: {e}" + except Exception as e: + error_message = f"System Error: {str(e)}" + + # Clean up file if it failed halfway through + if "zip_filename" in locals() and os.path.exists(zip_filename): + os.remove(zip_filename) + + return render_template( + "page1.html", action_result=action_result, error=error_message + ) + + +# --- PAGE 2: GCP Region Selector --- +@app.route("/deploy", methods=["GET", "POST"]) +def deploy_page(): + selected_region = None + api_response_data = None + operation_state = None + + if request.method == "POST": + selected_region = request.form.get("region") + action = request.form.get("action") + + if action == "deploy": + url = "https://apigee.saas8384.saas-example.com/v1/saas/operations" + headers = {"Content-Type": "application/json"} + payload = {"region": selected_region} + + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + api_response_data = response.json() + except requests.exceptions.RequestException as e: + api_response_data = {"error": str(e)} + + elif action == "status": + operation_id = "depr-25094d41-7768-4ef3-bd8d-e5bf67ca09d6" + url = ( + f"https://apigee.saas8384.saas-example.com/v1/saas/operations/" + f"{operation_id}?region={selected_region}" + ) + + try: + response = requests.get(url) + response.raise_for_status() + operation_state = response.json().get("state", "STATE_UNKNOWN") + except requests.exceptions.RequestException: + operation_state = "ERROR_FETCHING_STATE" + + return render_template( + "page2.html", + regions=GCP_REGIONS, + selected_region=selected_region, + api_response=api_response_data, + operation_state=operation_state, + ) + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 8080)) + app.run(host="0.0.0.0", port=port, debug=True) +# [END alm_ui_sample] diff --git a/alm/ui-examples/requirements.txt b/alm/ui-examples/requirements.txt new file mode 100644 index 00000000000..2d766d3e582 --- /dev/null +++ b/alm/ui-examples/requirements.txt @@ -0,0 +1,9 @@ +blinker==1.9.0 +click==8.4.1 +Flask==3.0.3 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +Werkzeug==3.1.8 +gunicorn +requests==2.34.2 \ No newline at end of file diff --git a/alm/ui-examples/static/style.css b/alm/ui-examples/static/style.css new file mode 100644 index 00000000000..0f5d4784bfe --- /dev/null +++ b/alm/ui-examples/static/style.css @@ -0,0 +1,169 @@ +/* Base Google/Material Styles */ +body { + font-family: 'Roboto', sans-serif; + background-color: #f1f3f4; + color: #202124; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; +} + +/* Mock GCP Console Header */ +.gcp-header { + width: 100%; + background-color: #ffffff; + border-bottom: 1px solid #dadce0; + padding: 12px 24px; + box-sizing: border-box; + display: flex; + align-items: center; + box-shadow: 0 1px 2px 0 rgba(60,64,67,0.3); +} + +.gcp-header h1 { + margin: 0; + font-size: 18px; + font-weight: 500; + color: #5f6368; +} + +/* Card Container for Content */ +.card { + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15); + width: 100%; + max-width: 450px; + padding: 24px; + margin-top: 60px; + box-sizing: border-box; +} + +.card h2 { + font-size: 22px; + font-weight: 400; + margin-top: 0; + margin-bottom: 24px; +} + +/* Form Elements */ +.form-group { + margin-bottom: 24px; +} + +label { + display: block; + font-size: 14px; + font-weight: 500; + margin-bottom: 8px; + color: #3c4043; +} + +select { + width: 100%; + padding: 10px 14px; + font-size: 14px; + font-family: 'Roboto', sans-serif; + color: #202124; + border: 1px solid #dadce0; + border-radius: 4px; + background-color: #fff; + appearance: none; + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%235f6368%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-repeat: no-repeat; + background-position: right 12px top 50%; + background-size: 10px auto; + transition: border-color 0.2s; +} + +select:focus { + outline: none; + border-color: #1a73e8; + box-shadow: inset 0 0 0 1px #1a73e8; +} + +/* Google Material Button */ +button { + background-color: #1a73e8; + color: white; + border: none; + border-radius: 4px; + padding: 10px 24px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + font-family: 'Roboto', sans-serif; + transition: background-color 0.2s, box-shadow 0.2s; +} + +button:hover { + background-color: #1b66c9; + box-shadow: 0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15); +} + +/* Success Alert */ +.result-alert { + margin-top: 24px; + padding: 16px; + background-color: #e8f0fe; + border-radius: 8px; + display: flex; + align-items: center; + font-size: 14px; + color: #1967d2; +} + +.result-alert svg { + margin-right: 12px; + fill: #1967d2; +} + + +/* Text Inputs (Matching your select styles) */ +input[type="text"] { + width: 100%; + padding: 10px 14px; + font-size: 14px; + font-family: 'Roboto', sans-serif; + color: #202124; + border: 1px solid #dadce0; + border-radius: 4px; + background-color: #fff; + box-sizing: border-box; /* Prevents it from spilling out of the container */ + transition: border-color 0.2s; + margin-top: 4px; +} + +input[type="text"]:focus { + outline: none; + border-color: #1a73e8; + box-shadow: inset 0 0 0 1px #1a73e8; +} + +/* Make sure your select also has box-sizing so they align perfectly */ +select { + box-sizing: border-box; +} + +/* Secondary Link Button (Next Page / Back) */ +.secondary-btn { + display: inline-block; + padding: 10px 24px; + background-color: #ffffff; + color: #1a73e8; + border: 1px solid #dadce0; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + text-decoration: none; + text-align: center; + transition: background-color 0.2s; + font-family: 'Roboto', sans-serif; +} + +.secondary-btn:hover { + background-color: #f8f9fa; +} \ No newline at end of file diff --git a/aml-ai/requirements-test.txt b/aml-ai/requirements-test.txt index 060ed652e0b..c9e154ba440 100644 --- a/aml-ai/requirements-test.txt +++ b/aml-ai/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/aml-ai/requirements.txt b/aml-ai/requirements.txt index 1c6bdbfe580..5323b82818c 100644 --- a/aml-ai/requirements.txt +++ b/aml-ai/requirements.txt @@ -1,4 +1,4 @@ google-api-python-client==2.131.0 google-auth-httplib2==0.2.0 google-auth==2.38.0 -requests==2.32.4 +requests==2.34.2; python_version >= '3.10' 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/analytics/noxfile_config.py b/appengine/flexible/analytics/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/analytics/noxfile_config.py +++ b/appengine/flexible/analytics/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": ["3.8", "3.9", "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/flexible/analytics/requirements-test.txt b/appengine/flexible/analytics/requirements-test.txt index 0a501624ec5..a4cc4887361 100644 --- a/appengine/flexible/analytics/requirements-test.txt +++ b/appengine/flexible/analytics/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" responses==0.23.1 diff --git a/appengine/flexible/analytics/requirements.txt b/appengine/flexible/analytics/requirements.txt index 3996cdf4450..28b44d78749 100644 --- a/appengine/flexible/analytics/requirements.txt +++ b/appengine/flexible/analytics/requirements.txt @@ -1,6 +1,4 @@ -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' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' gunicorn==23.0.0 -requests[security]==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/flexible/datastore/noxfile_config.py b/appengine/flexible/datastore/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/datastore/noxfile_config.py +++ b/appengine/flexible/datastore/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": ["3.8", "3.9", "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/flexible/datastore/requirements-test.txt b/appengine/flexible/datastore/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/datastore/requirements-test.txt +++ b/appengine/flexible/datastore/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/datastore/requirements.txt b/appengine/flexible/datastore/requirements.txt index 995f3365470..ae693495564 100644 --- a/appengine/flexible/datastore/requirements.txt +++ b/appengine/flexible/datastore/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' google-cloud-datastore==2.20.2 gunicorn==23.0.0 diff --git a/appengine/flexible/django_cloudsql/noxfile_config.py b/appengine/flexible/django_cloudsql/noxfile_config.py index 30010ba672d..fcf35546eb1 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": ["3.8", "3.9", "3.10", "3.11", "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/appengine/flexible/django_cloudsql/requirements-test.txt b/appengine/flexible/django_cloudsql/requirements-test.txt index 5e5d2c73a81..03bce874e92 100644 --- a/appengine/flexible/django_cloudsql/requirements-test.txt +++ b/appengine/flexible/django_cloudsql/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 -pytest-django==4.9.0 +pytest==9.0.3; python_version >= "3.10" +pytest-django==4.12.0 diff --git a/appengine/flexible/django_cloudsql/requirements.txt b/appengine/flexible/django_cloudsql/requirements.txt index 284290f2532..11fefcac54d 100644 --- a/appengine/flexible/django_cloudsql/requirements.txt +++ b/appengine/flexible/django_cloudsql/requirements.txt @@ -1,6 +1,6 @@ -Django==5.2.5 +Django==6.0.5; python_version >= "3.12" gunicorn==23.0.0 -psycopg2-binary==2.9.10 -django-environ==0.12.0 +psycopg2-binary==2.9.11 +django-environ==0.13.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..1514a56cb84 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.14 # 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/noxfile_config.py b/appengine/flexible/hello_world/noxfile_config.py index ae3ed14dc36..0973c8621c7 100644 --- a/appengine/flexible/hello_world/noxfile_config.py +++ b/appengine/flexible/hello_world/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": ["3.8", "3.9", "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": True, diff --git a/appengine/flexible/hello_world/requirements-test.txt b/appengine/flexible/hello_world/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/hello_world/requirements-test.txt +++ b/appengine/flexible/hello_world/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/hello_world/requirements.txt b/appengine/flexible/hello_world/requirements.txt index 068ea0acdfc..74b2a6cf56e 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.1.3; python_version >= '3.9' +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..132eb22ce81 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": ["3.8", "3.9", "3.10", "3.11", "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/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-test.txt b/appengine/flexible/hello_world_django/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/hello_world_django/requirements-test.txt +++ b/appengine/flexible/hello_world_django/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/hello_world_django/requirements.txt b/appengine/flexible/hello_world_django/requirements.txt index 564852cb740..c5f14c7e33d 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.5; python_version >= "3.12" gunicorn==23.0.0 diff --git a/appengine/flexible/metadata/noxfile_config.py b/appengine/flexible/metadata/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/metadata/noxfile_config.py +++ b/appengine/flexible/metadata/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": ["3.8", "3.9", "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/flexible/metadata/requirements-test.txt b/appengine/flexible/metadata/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/metadata/requirements-test.txt +++ b/appengine/flexible/metadata/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/metadata/requirements.txt b/appengine/flexible/metadata/requirements.txt index f2983c54b0e..fa909c4e380 100644 --- a/appengine/flexible/metadata/requirements.txt +++ b/appengine/flexible/metadata/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 -requests[security]==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/flexible/multiple_services/gateway-service/noxfile_config.py b/appengine/flexible/multiple_services/gateway-service/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/multiple_services/gateway-service/noxfile_config.py +++ b/appengine/flexible/multiple_services/gateway-service/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": ["3.8", "3.9", "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/flexible/multiple_services/gateway-service/requirements-test.txt b/appengine/flexible/multiple_services/gateway-service/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/multiple_services/gateway-service/requirements-test.txt +++ b/appengine/flexible/multiple_services/gateway-service/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/multiple_services/gateway-service/requirements.txt b/appengine/flexible/multiple_services/gateway-service/requirements.txt index daf3cbf7cdf..fa909c4e380 100644 --- a/appengine/flexible/multiple_services/gateway-service/requirements.txt +++ b/appengine/flexible/multiple_services/gateway-service/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/flexible/multiple_services/static-service/noxfile_config.py b/appengine/flexible/multiple_services/static-service/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/multiple_services/static-service/noxfile_config.py +++ b/appengine/flexible/multiple_services/static-service/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": ["3.8", "3.9", "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/flexible/multiple_services/static-service/requirements-test.txt b/appengine/flexible/multiple_services/static-service/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/multiple_services/static-service/requirements-test.txt +++ b/appengine/flexible/multiple_services/static-service/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/multiple_services/static-service/requirements.txt b/appengine/flexible/multiple_services/static-service/requirements.txt index daf3cbf7cdf..fa909c4e380 100644 --- a/appengine/flexible/multiple_services/static-service/requirements.txt +++ b/appengine/flexible/multiple_services/static-service/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/flexible/numpy/noxfile_config.py b/appengine/flexible/numpy/noxfile_config.py index 5f744eddc83..949abebe3ea 100644 --- a/appengine/flexible/numpy/noxfile_config.py +++ b/appengine/flexible/numpy/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.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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/flexible/numpy/requirements-test.txt b/appengine/flexible/numpy/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/numpy/requirements-test.txt +++ b/appengine/flexible/numpy/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/numpy/requirements.txt b/appengine/flexible/numpy/requirements.txt index 1e5cc4304ad..539b054530a 100644 --- a/appengine/flexible/numpy/requirements.txt +++ b/appengine/flexible/numpy/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 numpy==2.2.4; python_version > '3.9' numpy==1.26.4; python_version == '3.9' diff --git a/appengine/flexible/pubsub/noxfile_config.py b/appengine/flexible/pubsub/noxfile_config.py index 501440bf378..eb510f7a4b8 100644 --- a/appengine/flexible/pubsub/noxfile_config.py +++ b/appengine/flexible/pubsub/noxfile_config.py @@ -23,7 +23,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.7"], + "ignored_versions": ["3.8", "3.9", "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/flexible/pubsub/requirements-test.txt b/appengine/flexible/pubsub/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/pubsub/requirements-test.txt +++ b/appengine/flexible/pubsub/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/pubsub/requirements.txt b/appengine/flexible/pubsub/requirements.txt index 2c40e84343b..8d4f07227d5 100644 --- a/appengine/flexible/pubsub/requirements.txt +++ b/appengine/flexible/pubsub/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' google-cloud-pubsub==2.28.0 gunicorn==23.0.0 diff --git a/appengine/flexible/scipy/noxfile_config.py b/appengine/flexible/scipy/noxfile_config.py index fa718fc163c..6abbd44a54a 100644 --- a/appengine/flexible/scipy/noxfile_config.py +++ b/appengine/flexible/scipy/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.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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/flexible/scipy/requirements-test.txt b/appengine/flexible/scipy/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/scipy/requirements-test.txt +++ b/appengine/flexible/scipy/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/scipy/requirements.txt b/appengine/flexible/scipy/requirements.txt index fe4d29690ea..c10a69f6476 100644 --- a/appengine/flexible/scipy/requirements.txt +++ b/appengine/flexible/scipy/requirements.txt @@ -1,10 +1,10 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 imageio==2.35.1; python_version == '3.8' imageio==2.36.1; python_version >= '3.9' numpy==2.2.4; python_version > '3.9' numpy==1.26.4; python_version == '3.9' numpy==1.24.4; python_version == '3.8' -pillow==10.4.0 +pillow==12.3.0; python_version >= '3.10' scipy==1.10.1; python_version <= '3.9' scipy==1.14.1; python_version > '3.9' diff --git a/appengine/flexible/static_files/noxfile_config.py b/appengine/flexible/static_files/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/static_files/noxfile_config.py +++ b/appengine/flexible/static_files/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": ["3.8", "3.9", "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/flexible/static_files/requirements-test.txt b/appengine/flexible/static_files/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/static_files/requirements-test.txt +++ b/appengine/flexible/static_files/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/static_files/requirements.txt b/appengine/flexible/static_files/requirements.txt index 9ea9c8a9310..6b693f8f926 100644 --- a/appengine/flexible/static_files/requirements.txt +++ b/appengine/flexible/static_files/requirements.txt @@ -1,2 +1,2 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 diff --git a/appengine/flexible/storage/noxfile_config.py b/appengine/flexible/storage/noxfile_config.py index 972f04d9a4d..addd1f2d00e 100644 --- a/appengine/flexible/storage/noxfile_config.py +++ b/appengine/flexible/storage/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.6", "3.7", "3.8", "3.9", "3.10"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/appengine/flexible/storage/requirements-test.txt b/appengine/flexible/storage/requirements-test.txt index f27726d7455..3b2c5a8f638 100644 --- a/appengine/flexible/storage/requirements-test.txt +++ b/appengine/flexible/storage/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-storage==2.9.0 diff --git a/appengine/flexible/storage/requirements.txt b/appengine/flexible/storage/requirements.txt index 99fefaec60d..7f0f1104342 100644 --- a/appengine/flexible/storage/requirements.txt +++ b/appengine/flexible/storage/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' google-cloud-storage==2.9.0 gunicorn==23.0.0 diff --git a/appengine/flexible/tasks/Dockerfile b/appengine/flexible/tasks/Dockerfile index 5aaeb51144d..10d577925a9 100644 --- a/appengine/flexible/tasks/Dockerfile +++ b/appengine/flexible/tasks/Dockerfile @@ -14,7 +14,7 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.11 +FROM python:3.14 # Copy local code to the container image. ENV APP_HOME /app diff --git a/appengine/flexible/tasks/noxfile_config.py b/appengine/flexible/tasks/noxfile_config.py index 196376e7023..93284075acd 100644 --- a/appengine/flexible/tasks/noxfile_config.py +++ b/appengine/flexible/tasks/noxfile_config.py @@ -22,7 +22,8 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.7"], + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (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/appengine/flexible/tasks/requirements-test.txt b/appengine/flexible/tasks/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/appengine/flexible/tasks/requirements-test.txt +++ b/appengine/flexible/tasks/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/flexible/tasks/requirements.txt b/appengine/flexible/tasks/requirements.txt index 3a938a57ded..d3eb6cec11a 100644 --- a/appengine/flexible/tasks/requirements.txt +++ b/appengine/flexible/tasks/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 google-cloud-tasks==2.18.0 diff --git a/appengine/flexible/twilio/noxfile_config.py b/appengine/flexible/twilio/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/twilio/noxfile_config.py +++ b/appengine/flexible/twilio/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": ["3.8", "3.9", "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/flexible/twilio/requirements-test.txt b/appengine/flexible/twilio/requirements-test.txt index 0a501624ec5..a4cc4887361 100644 --- a/appengine/flexible/twilio/requirements-test.txt +++ b/appengine/flexible/twilio/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" responses==0.23.1 diff --git a/appengine/flexible/twilio/requirements.txt b/appengine/flexible/twilio/requirements.txt index 75303cef9ad..87973ae1e8d 100644 --- a/appengine/flexible/twilio/requirements.txt +++ b/appengine/flexible/twilio/requirements.txt @@ -1,6 +1,4 @@ -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' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' gunicorn==23.0.0 twilio==9.0.3 diff --git a/appengine/flexible/websockets/noxfile_config.py b/appengine/flexible/websockets/noxfile_config.py index 196376e7023..949abebe3ea 100644 --- a/appengine/flexible/websockets/noxfile_config.py +++ b/appengine/flexible/websockets/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": ["3.8", "3.9", "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/flexible/websockets/requirements-test.txt b/appengine/flexible/websockets/requirements-test.txt index 92b9194cf63..884df6e946a 100644 --- a/appengine/flexible/websockets/requirements-test.txt +++ b/appengine/flexible/websockets/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" retrying==1.3.4 websocket-client==1.7.0 diff --git a/appengine/flexible/websockets/requirements.txt b/appengine/flexible/websockets/requirements.txt index c1525d36077..eb3d426fe96 100644 --- a/appengine/flexible/websockets/requirements.txt +++ b/appengine/flexible/websockets/requirements.txt @@ -1,6 +1,6 @@ -Flask==1.1.4 # it seems like Flask-sockets doesn't play well with 2.0+ +Flask==3.1.3; python_version >= '3.9' # 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 +requests==2.34.2; python_version >= '3.10' markupsafe==2.0.1 -Werkzeug==1.0.1; +Werkzeug==3.1.8; python_version >= '3.9' 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/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/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/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/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/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/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/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/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/app.yaml b/appengine/flexible_python37_and_earlier/metadata/app.yaml deleted file mode 100644 index ca76f83fc3b..00000000000 --- a/appengine/flexible_python37_and_earlier/metadata/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/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/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/analytics/requirements-test.txt b/appengine/standard/analytics/requirements-test.txt index 30b5b4c9f19..1f1919df7e1 100644 --- a/appengine/standard/analytics/requirements-test.txt +++ b/appengine/standard/analytics/requirements-test.txt @@ -1,4 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" responses==0.17.0; python_version < '3.7' responses==0.23.1; python_version > '3.6' diff --git a/appengine/standard/analytics/requirements.txt b/appengine/standard/analytics/requirements.txt index 22b490a10fe..57db758ea84 100644 --- a/appengine/standard/analytics/requirements.txt +++ b/appengine/standard/analytics/requirements.txt @@ -1,6 +1,4 @@ -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -requests==2.27.1 -requests-toolbelt==0.10.1 -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' +requests-toolbelt==1.0.0 diff --git a/appengine/standard/blobstore/blobreader/requirements-test.txt b/appengine/standard/blobstore/blobreader/requirements-test.txt index c607ba3b2ab..201a14af7e3 100644 --- a/appengine/standard/blobstore/blobreader/requirements-test.txt +++ b/appengine/standard/blobstore/blobreader/requirements-test.txt @@ -1,3 +1,2 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" WebTest==2.0.35; python_version < '3.0' diff --git a/appengine/standard/blobstore/gcs/requirements-test.txt b/appengine/standard/blobstore/gcs/requirements-test.txt index c607ba3b2ab..201a14af7e3 100644 --- a/appengine/standard/blobstore/gcs/requirements-test.txt +++ b/appengine/standard/blobstore/gcs/requirements-test.txt @@ -1,3 +1,2 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" WebTest==2.0.35; python_version < '3.0' diff --git a/appengine/standard/endpoints-frameworks-v2/echo/requirements-test.txt b/appengine/standard/endpoints-frameworks-v2/echo/requirements-test.txt index b45b8adfc17..c6c555e9b7d 100644 --- a/appengine/standard/endpoints-frameworks-v2/echo/requirements-test.txt +++ b/appengine/standard/endpoints-frameworks-v2/echo/requirements-test.txt @@ -1,5 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' -pytest==8.3.2; python_version >= '3.0' +pytest==9.0.3; python_version >= "3.10" mock==3.0.5; python_version < '3.0' mock==5.1.0; python_version >= '3.0' diff --git a/appengine/standard/endpoints-frameworks-v2/iata/requirements-test.txt b/appengine/standard/endpoints-frameworks-v2/iata/requirements-test.txt index 7439fc43d48..c9e154ba440 100644 --- a/appengine/standard/endpoints-frameworks-v2/iata/requirements-test.txt +++ b/appengine/standard/endpoints-frameworks-v2/iata/requirements-test.txt @@ -1,2 +1 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard/endpoints-frameworks-v2/quickstart/requirements-test.txt b/appengine/standard/endpoints-frameworks-v2/quickstart/requirements-test.txt index b45b8adfc17..c6c555e9b7d 100644 --- a/appengine/standard/endpoints-frameworks-v2/quickstart/requirements-test.txt +++ b/appengine/standard/endpoints-frameworks-v2/quickstart/requirements-test.txt @@ -1,5 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' -pytest==8.3.2; python_version >= '3.0' +pytest==9.0.3; python_version >= "3.10" mock==3.0.5; python_version < '3.0' mock==5.1.0; python_version >= '3.0' diff --git a/appengine/standard/firebase/firenotes/backend/requirements-test.txt b/appengine/standard/firebase/firenotes/backend/requirements-test.txt index b45b8adfc17..c6c555e9b7d 100644 --- a/appengine/standard/firebase/firenotes/backend/requirements-test.txt +++ b/appengine/standard/firebase/firenotes/backend/requirements-test.txt @@ -1,5 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' -pytest==8.3.2; python_version >= '3.0' +pytest==9.0.3; python_version >= "3.10" mock==3.0.5; python_version < '3.0' mock==5.1.0; python_version >= '3.0' diff --git a/appengine/standard/firebase/firenotes/backend/requirements.txt b/appengine/standard/firebase/firenotes/backend/requirements.txt index e9d74191918..1d7d0658ec0 100644 --- a/appengine/standard/firebase/firenotes/backend/requirements.txt +++ b/appengine/standard/firebase/firenotes/backend/requirements.txt @@ -1,10 +1,8 @@ -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' pyjwt==1.7.1; python_version < '3.0' flask-cors==6.0.0 google-auth==2.17.3; python_version < '3.0' google-auth==2.17.3; python_version > '3.0' -requests==2.27.1 -requests-toolbelt==0.10.1 -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Werkzeug==3.1.8; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' +requests-toolbelt==1.0.0 diff --git a/appengine/standard/flask/hello_world/requirements-test.txt b/appengine/standard/flask/hello_world/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/flask/hello_world/requirements-test.txt +++ b/appengine/standard/flask/hello_world/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/flask/hello_world/requirements.txt b/appengine/standard/flask/hello_world/requirements.txt index 8b137891791..e69de29bb2d 100644 --- a/appengine/standard/flask/hello_world/requirements.txt +++ b/appengine/standard/flask/hello_world/requirements.txt @@ -1 +0,0 @@ - diff --git a/appengine/standard/iap/requirements-test.txt b/appengine/standard/iap/requirements-test.txt index 2bdf05ee6cd..645c7b65f82 100644 --- a/appengine/standard/iap/requirements-test.txt +++ b/appengine/standard/iap/requirements-test.txt @@ -1,5 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' -pytest==8.3.2; python_version >= '3.0' +pytest==9.0.3; python_version >= "3.10" WebTest==2.0.35; python_version < '3.0' six==1.16.0 diff --git a/appengine/standard/iap/requirements.txt b/appengine/standard/iap/requirements.txt index 118baae465c..f20de702c73 100644 --- a/appengine/standard/iap/requirements.txt +++ b/appengine/standard/iap/requirements.txt @@ -1,5 +1,3 @@ -Flask==1.1.4; python_version < '3.0' -Flask==2.1.0; python_version > '3.0' -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' six==1.16.0 diff --git a/appengine/standard/images/api/requirements-test.txt b/appengine/standard/images/api/requirements-test.txt index e32096ac3f2..e44de1e709c 100644 --- a/appengine/standard/images/api/requirements-test.txt +++ b/appengine/standard/images/api/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.3.5 +pytest==9.0.3; python_version >= "3.10" six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/memcache/guestbook/requirements-test.txt b/appengine/standard/memcache/guestbook/requirements-test.txt index fc0672f932b..e44de1e709c 100644 --- a/appengine/standard/memcache/guestbook/requirements-test.txt +++ b/appengine/standard/memcache/guestbook/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.3.4 +pytest==9.0.3; python_version >= "3.10" six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/memcache/snippets/requirements-test.txt b/appengine/standard/memcache/snippets/requirements-test.txt index fc0672f932b..e44de1e709c 100644 --- a/appengine/standard/memcache/snippets/requirements-test.txt +++ b/appengine/standard/memcache/snippets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.3.4 +pytest==9.0.3; python_version >= "3.10" six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/migration/incoming/requirements-test.txt b/appengine/standard/migration/incoming/requirements-test.txt index 9dddb06acfc..81aa28131ef 100644 --- a/appengine/standard/migration/incoming/requirements-test.txt +++ b/appengine/standard/migration/incoming/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.3.5 +pytest==9.0.3; python_version >= "3.10" WebTest==3.0.4 diff --git a/appengine/standard/migration/incoming/requirements.txt b/appengine/standard/migration/incoming/requirements.txt index 1b6d8a6ee2f..b415937ef8a 100644 --- a/appengine/standard/migration/incoming/requirements.txt +++ b/appengine/standard/migration/incoming/requirements.txt @@ -1,2 +1,2 @@ google-auth==2.17.3 -requests==2.27.1 +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard/migration/memorystore/requirements-test.txt b/appengine/standard/migration/memorystore/requirements-test.txt index 97587b45b12..8f9dd472203 100644 --- a/appengine/standard/migration/memorystore/requirements-test.txt +++ b/appengine/standard/migration/memorystore/requirements-test.txt @@ -1,6 +1,4 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' -pytest==8.3.2; python_version >= '3.0' +pytest==9.0.3; python_version >= "3.10" mock==5.0.2; python_version > '3.0' mock==3.0.5; python_version < '3.0' six==1.16.0 diff --git a/appengine/standard/migration/memorystore/requirements.txt b/appengine/standard/migration/memorystore/requirements.txt index 23045baf750..eae19eaa0a4 100644 --- a/appengine/standard/migration/memorystore/requirements.txt +++ b/appengine/standard/migration/memorystore/requirements.txt @@ -1,6 +1,4 @@ redis==4.5.4; python_version > '3.0' redis<5; python_version < '3.0' -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/appengine/standard/migration/ndb/overview/requirements-test.txt b/appengine/standard/migration/ndb/overview/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/migration/ndb/overview/requirements-test.txt +++ b/appengine/standard/migration/ndb/overview/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/migration/ndb/overview/requirements.txt b/appengine/standard/migration/ndb/overview/requirements.txt index b728da87752..87fb3651f86 100644 --- a/appengine/standard/migration/ndb/overview/requirements.txt +++ b/appengine/standard/migration/ndb/overview/requirements.txt @@ -2,7 +2,5 @@ rsa==4.5; python_version < '3' googleapis_common_protos google-cloud-ndb -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/appengine/standard/migration/ndb/redis_cache/requirements-test.txt b/appengine/standard/migration/ndb/redis_cache/requirements-test.txt index 729e42b9a2e..325a4c49696 100644 --- a/appengine/standard/migration/ndb/redis_cache/requirements-test.txt +++ b/appengine/standard/migration/ndb/redis_cache/requirements-test.txt @@ -1,7 +1,5 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" # 2025-01-14 Adds support for Python3. -pytest==8.3.2; python_version >= '3.0' WebTest==3.0.1; python_version >= '3.0' six==1.16.0 \ No newline at end of file diff --git a/appengine/standard/migration/ndb/redis_cache/requirements.txt b/appengine/standard/migration/ndb/redis_cache/requirements.txt index b728da87752..87fb3651f86 100644 --- a/appengine/standard/migration/ndb/redis_cache/requirements.txt +++ b/appengine/standard/migration/ndb/redis_cache/requirements.txt @@ -2,7 +2,5 @@ rsa==4.5; python_version < '3' googleapis_common_protos google-cloud-ndb -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/appengine/standard/migration/storage/requirements-test.txt b/appengine/standard/migration/storage/requirements-test.txt index 7439fc43d48..c9e154ba440 100644 --- a/appengine/standard/migration/storage/requirements-test.txt +++ b/appengine/standard/migration/storage/requirements-test.txt @@ -1,2 +1 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard/migration/storage/requirements.txt b/appengine/standard/migration/storage/requirements.txt index 78127d4f6e2..39a44d16518 100644 --- a/appengine/standard/migration/storage/requirements.txt +++ b/appengine/standard/migration/storage/requirements.txt @@ -1,6 +1,4 @@ google-cloud-storage==1.44.0; python_version < '3.7' google-cloud-storage==2.8.0; python_version > '3.6' -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/appengine/standard/migration/taskqueue/pull-counter/requirements-test.txt b/appengine/standard/migration/taskqueue/pull-counter/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/migration/taskqueue/pull-counter/requirements-test.txt +++ b/appengine/standard/migration/taskqueue/pull-counter/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/migration/taskqueue/pull-counter/requirements.txt b/appengine/standard/migration/taskqueue/pull-counter/requirements.txt index 76c398af459..a1fcde1e7f2 100644 --- a/appengine/standard/migration/taskqueue/pull-counter/requirements.txt +++ b/appengine/standard/migration/taskqueue/pull-counter/requirements.txt @@ -1,9 +1,4 @@ -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' google-cloud-datastore==2.15.1 ; python_version >= "3.0" -google-cloud-datastore<2 ; python_version < "3.0" google-cloud-pubsub==2.16.0 ; python_version >= "3.0" -# 1.7.0 is the latest compatible version for Python 2. -google-cloud-pubsub==1.7.2 ; python_version < "3.0" -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Werkzeug==3.1.8; python_version >= '3.9' \ No newline at end of file diff --git a/appengine/standard/migration/urlfetch/async/requirements-test.txt b/appengine/standard/migration/urlfetch/async/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/migration/urlfetch/async/requirements-test.txt +++ b/appengine/standard/migration/urlfetch/async/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/migration/urlfetch/async/requirements.txt b/appengine/standard/migration/urlfetch/async/requirements.txt index 86a92de08d2..f0ccd6b272e 100644 --- a/appengine/standard/migration/urlfetch/async/requirements.txt +++ b/appengine/standard/migration/urlfetch/async/requirements.txt @@ -1,7 +1,5 @@ -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -requests==2.27.1 -requests-futures==1.0.0 -requests-toolbelt==0.10.1 -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' +requests-futures==1.0.2 +requests-toolbelt==1.0.0 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/appengine/standard/migration/urlfetch/requests/requirements-test.txt b/appengine/standard/migration/urlfetch/requests/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/migration/urlfetch/requests/requirements-test.txt +++ b/appengine/standard/migration/urlfetch/requests/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/migration/urlfetch/requests/requirements.txt b/appengine/standard/migration/urlfetch/requests/requirements.txt index 22b490a10fe..57db758ea84 100644 --- a/appengine/standard/migration/urlfetch/requests/requirements.txt +++ b/appengine/standard/migration/urlfetch/requests/requirements.txt @@ -1,6 +1,4 @@ -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -requests==2.27.1 -requests-toolbelt==0.10.1 -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' +requests-toolbelt==1.0.0 diff --git a/appengine/standard/ndb/overview/requirements-test.txt b/appengine/standard/ndb/overview/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/ndb/overview/requirements-test.txt +++ b/appengine/standard/ndb/overview/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/ndb/transactions/requirements-test.txt b/appengine/standard/ndb/transactions/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/ndb/transactions/requirements-test.txt +++ b/appengine/standard/ndb/transactions/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/ndb/transactions/requirements.txt b/appengine/standard/ndb/transactions/requirements.txt index aca673e4e0d..76aaddfd791 100644 --- a/appengine/standard/ndb/transactions/requirements.txt +++ b/appengine/standard/ndb/transactions/requirements.txt @@ -1,4 +1,2 @@ -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' \ No newline at end of file +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' 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..c58186d0158 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.8", "3.9", "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/taskqueue/counter/requirements-test.txt b/appengine/standard/taskqueue/counter/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/taskqueue/counter/requirements-test.txt +++ b/appengine/standard/taskqueue/counter/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/taskqueue/counter/requirements.txt b/appengine/standard/taskqueue/counter/requirements.txt index 8b137891791..e69de29bb2d 100644 --- a/appengine/standard/taskqueue/counter/requirements.txt +++ b/appengine/standard/taskqueue/counter/requirements.txt @@ -1 +0,0 @@ - diff --git a/appengine/standard/taskqueue/pull-counter/requirements-test.txt b/appengine/standard/taskqueue/pull-counter/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/taskqueue/pull-counter/requirements-test.txt +++ b/appengine/standard/taskqueue/pull-counter/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/urlfetch/async/requirements-test.txt b/appengine/standard/urlfetch/async/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/urlfetch/async/requirements-test.txt +++ b/appengine/standard/urlfetch/async/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/urlfetch/requests/requirements-test.txt b/appengine/standard/urlfetch/requests/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/urlfetch/requests/requirements-test.txt +++ b/appengine/standard/urlfetch/requests/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/urlfetch/requests/requirements.txt b/appengine/standard/urlfetch/requests/requirements.txt index 22b490a10fe..57db758ea84 100644 --- a/appengine/standard/urlfetch/requests/requirements.txt +++ b/appengine/standard/urlfetch/requests/requirements.txt @@ -1,6 +1,4 @@ -Flask==1.1.4; python_version < '3.0' -Flask==3.0.0; python_version > '3.0' -requests==2.27.1 -requests-toolbelt==0.10.1 -Werkzeug==1.0.1; python_version < '3.0' -Werkzeug==3.0.3; python_version > '3.0' +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' +requests-toolbelt==1.0.0 diff --git a/appengine/standard/urlfetch/snippets/requirements-test.txt b/appengine/standard/urlfetch/snippets/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/urlfetch/snippets/requirements-test.txt +++ b/appengine/standard/urlfetch/snippets/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard/users/requirements-test.txt b/appengine/standard/users/requirements-test.txt index 454c88a573a..e7fb62924c9 100644 --- a/appengine/standard/users/requirements-test.txt +++ b/appengine/standard/users/requirements-test.txt @@ -1,6 +1,3 @@ -# pin pytest to 4.6.11 for Python2. -pytest==4.6.11; python_version < '3.0' +pytest==9.0.3; python_version >= "3.10" -# pytest==8.3.4 and six==1.17.0 for Python3. -pytest==8.3.4; python_version >= '3.0' six==1.17.0 \ No newline at end of file diff --git a/appengine/standard_python3/bigquery/app.yaml b/appengine/standard_python3/bigquery/app.yaml index 83c91f5b872..472f1f0c034 100644 --- a/appengine/standard_python3/bigquery/app.yaml +++ b/appengine/standard_python3/bigquery/app.yaml @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 diff --git a/appengine/standard_python3/bigquery/noxfile_config.py b/appengine/standard_python3/bigquery/noxfile_config.py index 3428b4ef27a..5dbb51286c6 100644 --- a/appengine/standard_python3/bigquery/noxfile_config.py +++ b/appengine/standard_python3/bigquery/noxfile_config.py @@ -23,7 +23,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. # There's no google-cloud-bigquery package for Python 3.9. - "ignored_versions": ["2.7", "3.6", "3.9", "3.11"], + "ignored_versions": ["3.8", "3.9", "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/bigquery/requirements-test.txt b/appengine/standard_python3/bigquery/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/bigquery/requirements-test.txt +++ b/appengine/standard_python3/bigquery/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/bigquery/requirements.txt b/appengine/standard_python3/bigquery/requirements.txt index d27f78612de..c89c5277eee 100644 --- a/appengine/standard_python3/bigquery/requirements.txt +++ b/appengine/standard_python3/bigquery/requirements.txt @@ -1,2 +1,2 @@ google-cloud-bigquery==3.10.0 -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' 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..c9e154ba440 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 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/building-an-app/building-an-app-1/requirements.txt b/appengine/standard_python3/building-an-app/building-an-app-1/requirements.txt index 047e9501aa8..b3ff32e568d 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-1/requirements.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-1/requirements.txt @@ -1 +1 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' diff --git a/appengine/standard_python3/building-an-app/building-an-app-2/app.yaml b/appengine/standard_python3/building-an-app/building-an-app-2/app.yaml index a0931a8a5d9..100d540982b 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-2/app.yaml +++ b/appengine/standard_python3/building-an-app/building-an-app-2/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 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-2/requirements-test.txt b/appengine/standard_python3/building-an-app/building-an-app-2/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-2/requirements-test.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-2/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/building-an-app/building-an-app-2/requirements.txt b/appengine/standard_python3/building-an-app/building-an-app-2/requirements.txt index 13f13f5bbc5..43f9b65de95 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-2/requirements.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-2/requirements.txt @@ -1,2 +1,2 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' google-cloud-datastore==2.15.1 diff --git a/appengine/standard_python3/building-an-app/building-an-app-3/app.yaml b/appengine/standard_python3/building-an-app/building-an-app-3/app.yaml index a0931a8a5d9..100d540982b 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-3/app.yaml +++ b/appengine/standard_python3/building-an-app/building-an-app-3/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 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-3/requirements-test.txt b/appengine/standard_python3/building-an-app/building-an-app-3/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-3/requirements-test.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-3/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/building-an-app/building-an-app-3/requirements.txt b/appengine/standard_python3/building-an-app/building-an-app-3/requirements.txt index 8610c24825f..180ee8d7bd8 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-3/requirements.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-3/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' google-cloud-datastore==2.15.1 google-auth==2.17.3 -requests==2.28.2 +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/building-an-app/building-an-app-4/app.yaml b/appengine/standard_python3/building-an-app/building-an-app-4/app.yaml index a0931a8a5d9..100d540982b 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-4/app.yaml +++ b/appengine/standard_python3/building-an-app/building-an-app-4/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 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-4/requirements-test.txt b/appengine/standard_python3/building-an-app/building-an-app-4/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-4/requirements-test.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-4/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/building-an-app/building-an-app-4/requirements.txt b/appengine/standard_python3/building-an-app/building-an-app-4/requirements.txt index 8610c24825f..180ee8d7bd8 100644 --- a/appengine/standard_python3/building-an-app/building-an-app-4/requirements.txt +++ b/appengine/standard_python3/building-an-app/building-an-app-4/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' google-cloud-datastore==2.15.1 google-auth==2.17.3 -requests==2.28.2 +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/blobstore/django/app.yaml b/appengine/standard_python3/bundled-services/blobstore/django/app.yaml index 96e1c924ee3..6994339e157 100644 --- a/appengine/standard_python3/bundled-services/blobstore/django/app.yaml +++ b/appengine/standard_python3/bundled-services/blobstore/django/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 app_engine_apis: true handlers: diff --git a/appengine/standard_python3/bundled-services/blobstore/django/noxfile_config.py b/appengine/standard_python3/bundled-services/blobstore/django/noxfile_config.py index 1bde00988d8..57986117068 100644 --- a/appengine/standard_python3/bundled-services/blobstore/django/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/blobstore/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", "3.8", "3.10", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "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/bundled-services/blobstore/django/requirements-test.txt b/appengine/standard_python3/bundled-services/blobstore/django/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/blobstore/django/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/blobstore/django/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/blobstore/django/requirements.txt b/appengine/standard_python3/bundled-services/blobstore/django/requirements.txt index c616634cafe..5d719f78788 100644 --- a/appengine/standard_python3/bundled-services/blobstore/django/requirements.txt +++ b/appengine/standard_python3/bundled-services/blobstore/django/requirements.txt @@ -1,5 +1,4 @@ -Django==5.1.9; python_version >= "3.10" -Django==4.2.16; python_version < "3.10" -django-environ==0.10.0 +Django==6.0.5; python_version >= "3.12" +django-environ==0.13.0 google-cloud-logging==3.5.0 appengine-python-standard>=0.2.3 diff --git a/appengine/standard_python3/bundled-services/blobstore/flask/app.yaml b/appengine/standard_python3/bundled-services/blobstore/flask/app.yaml index 96e1c924ee3..6994339e157 100644 --- a/appengine/standard_python3/bundled-services/blobstore/flask/app.yaml +++ b/appengine/standard_python3/bundled-services/blobstore/flask/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 app_engine_apis: true handlers: diff --git a/appengine/standard_python3/bundled-services/blobstore/flask/noxfile_config.py b/appengine/standard_python3/bundled-services/blobstore/flask/noxfile_config.py index 1bde00988d8..99047da44a9 100644 --- a/appengine/standard_python3/bundled-services/blobstore/flask/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/blobstore/flask/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": ["3.8", "3.9", "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/bundled-services/blobstore/flask/requirements-test.txt b/appengine/standard_python3/bundled-services/blobstore/flask/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/blobstore/flask/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/blobstore/flask/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/blobstore/flask/requirements.txt b/appengine/standard_python3/bundled-services/blobstore/flask/requirements.txt index 2d4ffcf5864..cec709a1635 100644 --- a/appengine/standard_python3/bundled-services/blobstore/flask/requirements.txt +++ b/appengine/standard_python3/bundled-services/blobstore/flask/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' appengine-python-standard>=0.2.3 -Werkzeug==3.0.3 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/appengine/standard_python3/bundled-services/blobstore/wsgi/app.yaml b/appengine/standard_python3/bundled-services/blobstore/wsgi/app.yaml index 96e1c924ee3..6994339e157 100644 --- a/appengine/standard_python3/bundled-services/blobstore/wsgi/app.yaml +++ b/appengine/standard_python3/bundled-services/blobstore/wsgi/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 app_engine_apis: true handlers: diff --git a/appengine/standard_python3/bundled-services/blobstore/wsgi/noxfile_config.py b/appengine/standard_python3/bundled-services/blobstore/wsgi/noxfile_config.py index 1bde00988d8..99047da44a9 100644 --- a/appengine/standard_python3/bundled-services/blobstore/wsgi/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/blobstore/wsgi/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": ["3.8", "3.9", "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/bundled-services/blobstore/wsgi/requirements-test.txt b/appengine/standard_python3/bundled-services/blobstore/wsgi/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/blobstore/wsgi/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/blobstore/wsgi/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/deferred/django/app.yaml b/appengine/standard_python3/bundled-services/deferred/django/app.yaml index 84314e1d25b..c2226a56b67 100644 --- a/appengine/standard_python3/bundled-services/deferred/django/app.yaml +++ b/appengine/standard_python3/bundled-services/deferred/django/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 app_engine_apis: true env_variables: NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL: "True" diff --git a/appengine/standard_python3/bundled-services/deferred/django/noxfile_config.py b/appengine/standard_python3/bundled-services/deferred/django/noxfile_config.py index 1bde00988d8..57986117068 100644 --- a/appengine/standard_python3/bundled-services/deferred/django/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/deferred/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", "3.8", "3.10", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "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/bundled-services/deferred/django/requirements-test.txt b/appengine/standard_python3/bundled-services/deferred/django/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/deferred/django/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/deferred/django/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/deferred/django/requirements.txt b/appengine/standard_python3/bundled-services/deferred/django/requirements.txt index be7bb5d29a8..c99a837b0b2 100644 --- a/appengine/standard_python3/bundled-services/deferred/django/requirements.txt +++ b/appengine/standard_python3/bundled-services/deferred/django/requirements.txt @@ -1,6 +1,4 @@ -Django==5.1.7; python_version >= "3.10" -Django==4.2.16; python_version >= "3.8" and python_version < "3.10" -Django==3.2.25; python_version < "3.8" -django-environ==0.10.0 +Django==6.0.5; python_version >= "3.12" +django-environ==0.13.0 google-cloud-logging==3.5.0 appengine-python-standard>=0.3.1 diff --git a/appengine/standard_python3/bundled-services/deferred/flask/app.yaml b/appengine/standard_python3/bundled-services/deferred/flask/app.yaml index 84314e1d25b..c2226a56b67 100644 --- a/appengine/standard_python3/bundled-services/deferred/flask/app.yaml +++ b/appengine/standard_python3/bundled-services/deferred/flask/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 app_engine_apis: true env_variables: NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL: "True" diff --git a/appengine/standard_python3/bundled-services/deferred/flask/noxfile_config.py b/appengine/standard_python3/bundled-services/deferred/flask/noxfile_config.py index 1bde00988d8..99047da44a9 100644 --- a/appengine/standard_python3/bundled-services/deferred/flask/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/deferred/flask/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": ["3.8", "3.9", "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/bundled-services/deferred/flask/requirements-test.txt b/appengine/standard_python3/bundled-services/deferred/flask/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/deferred/flask/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/deferred/flask/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/deferred/flask/requirements.txt b/appengine/standard_python3/bundled-services/deferred/flask/requirements.txt index 63f4b5339b1..75b0329ce07 100644 --- a/appengine/standard_python3/bundled-services/deferred/flask/requirements.txt +++ b/appengine/standard_python3/bundled-services/deferred/flask/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' appengine-python-standard>=0.3.1 -Werkzeug==3.0.3 \ No newline at end of file +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/appengine/standard_python3/bundled-services/deferred/wsgi/app.yaml b/appengine/standard_python3/bundled-services/deferred/wsgi/app.yaml index 84314e1d25b..c2226a56b67 100644 --- a/appengine/standard_python3/bundled-services/deferred/wsgi/app.yaml +++ b/appengine/standard_python3/bundled-services/deferred/wsgi/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 app_engine_apis: true env_variables: NDB_USE_CROSS_COMPATIBLE_PICKLE_PROTOCOL: "True" diff --git a/appengine/standard_python3/bundled-services/deferred/wsgi/noxfile_config.py b/appengine/standard_python3/bundled-services/deferred/wsgi/noxfile_config.py index 1bde00988d8..99047da44a9 100644 --- a/appengine/standard_python3/bundled-services/deferred/wsgi/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/deferred/wsgi/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": ["3.8", "3.9", "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/bundled-services/deferred/wsgi/requirements-test.txt b/appengine/standard_python3/bundled-services/deferred/wsgi/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/deferred/wsgi/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/deferred/wsgi/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/mail/django/app.yaml b/appengine/standard_python3/bundled-services/mail/django/app.yaml index ff79a69182c..902fe897910 100644 --- a/appengine/standard_python3/bundled-services/mail/django/app.yaml +++ b/appengine/standard_python3/bundled-services/mail/django/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 app_engine_apis: true inbound_services: diff --git a/appengine/standard_python3/bundled-services/mail/django/noxfile_config.py b/appengine/standard_python3/bundled-services/mail/django/noxfile_config.py index 1bde00988d8..57986117068 100644 --- a/appengine/standard_python3/bundled-services/mail/django/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/mail/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", "3.8", "3.10", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "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/bundled-services/mail/django/requirements-test.txt b/appengine/standard_python3/bundled-services/mail/django/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/mail/django/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/mail/django/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/mail/django/requirements.txt b/appengine/standard_python3/bundled-services/mail/django/requirements.txt index aa420e655c1..5d719f78788 100644 --- a/appengine/standard_python3/bundled-services/mail/django/requirements.txt +++ b/appengine/standard_python3/bundled-services/mail/django/requirements.txt @@ -1,6 +1,4 @@ -Django==5.1.12; python_version >= "3.10" -Django==4.2.16; python_version >= "3.8" and python_version < "3.10" -Django==3.2.25; python_version < "3.8" -django-environ==0.10.0 +Django==6.0.5; python_version >= "3.12" +django-environ==0.13.0 google-cloud-logging==3.5.0 appengine-python-standard>=0.2.3 diff --git a/appengine/standard_python3/bundled-services/mail/flask/app.yaml b/appengine/standard_python3/bundled-services/mail/flask/app.yaml index ff79a69182c..79f6d993358 100644 --- a/appengine/standard_python3/bundled-services/mail/flask/app.yaml +++ b/appengine/standard_python3/bundled-services/mail/flask/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python312 app_engine_apis: true inbound_services: diff --git a/appengine/standard_python3/bundled-services/mail/flask/noxfile_config.py b/appengine/standard_python3/bundled-services/mail/flask/noxfile_config.py index 1bde00988d8..99047da44a9 100644 --- a/appengine/standard_python3/bundled-services/mail/flask/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/mail/flask/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": ["3.8", "3.9", "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/bundled-services/mail/flask/requirements-test.txt b/appengine/standard_python3/bundled-services/mail/flask/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/mail/flask/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/mail/flask/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/bundled-services/mail/flask/requirements.txt b/appengine/standard_python3/bundled-services/mail/flask/requirements.txt index ea7004ffc92..e9946967ce1 100644 --- a/appengine/standard_python3/bundled-services/mail/flask/requirements.txt +++ b/appengine/standard_python3/bundled-services/mail/flask/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' appengine-python-standard==1.1.4 -Werkzeug==3.0.3 -MarkupSafe==2.1.3 \ No newline at end of file +Werkzeug==3.1.8; python_version >= '3.9' +MarkupSafe==2.1.3 diff --git a/appengine/standard_python3/bundled-services/mail/wsgi/app.yaml b/appengine/standard_python3/bundled-services/mail/wsgi/app.yaml index ff79a69182c..79f6d993358 100644 --- a/appengine/standard_python3/bundled-services/mail/wsgi/app.yaml +++ b/appengine/standard_python3/bundled-services/mail/wsgi/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python312 app_engine_apis: true inbound_services: diff --git a/appengine/standard_python3/bundled-services/mail/wsgi/noxfile_config.py b/appengine/standard_python3/bundled-services/mail/wsgi/noxfile_config.py index 1bde00988d8..99047da44a9 100644 --- a/appengine/standard_python3/bundled-services/mail/wsgi/noxfile_config.py +++ b/appengine/standard_python3/bundled-services/mail/wsgi/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": ["3.8", "3.9", "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/bundled-services/mail/wsgi/requirements-test.txt b/appengine/standard_python3/bundled-services/mail/wsgi/requirements-test.txt index b83aedab5dd..6304310755c 100644 --- a/appengine/standard_python3/bundled-services/mail/wsgi/requirements-test.txt +++ b/appengine/standard_python3/bundled-services/mail/wsgi/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==7.1.2 -requests==2.28.2 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/appengine/standard_python3/cloudsql/app.yaml b/appengine/standard_python3/cloudsql/app.yaml index 496b60f231b..dfb14663846 100644 --- a/appengine/standard_python3/cloudsql/app.yaml +++ b/appengine/standard_python3/cloudsql/app.yaml @@ -14,7 +14,7 @@ # [START gae_python38_cloudsql_config] # [START gae_python3_cloudsql_config] -runtime: python39 +runtime: python313 env_variables: CLOUD_SQL_USERNAME: YOUR-USERNAME diff --git a/appengine/standard_python3/cloudsql/requirements-test.txt b/appengine/standard_python3/cloudsql/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/cloudsql/requirements-test.txt +++ b/appengine/standard_python3/cloudsql/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/cloudsql/requirements.txt b/appengine/standard_python3/cloudsql/requirements.txt index 7ca534fe2e0..92a0ae9d059 100644 --- a/appengine/standard_python3/cloudsql/requirements.txt +++ b/appengine/standard_python3/cloudsql/requirements.txt @@ -1,6 +1,6 @@ -flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' # psycopg2==2.8.4 # you will need either the binary or the regular - for more info see http://initd.org/psycopg/docs/install.html -psycopg2-binary==2.9.9 +psycopg2-binary==2.9.11 PyMySQL==1.1.1 -SQLAlchemy==2.0.10 \ No newline at end of file +SQLAlchemy==2.0.44 diff --git a/appengine/standard_python3/custom-server/app.yaml b/appengine/standard_python3/custom-server/app.yaml index ff2f64b2b26..b67aef4f96e 100644 --- a/appengine/standard_python3/custom-server/app.yaml +++ b/appengine/standard_python3/custom-server/app.yaml @@ -14,7 +14,7 @@ # [START gae_python38_custom_runtime] # [START gae_python3_custom_runtime] -runtime: python39 +runtime: python313 entrypoint: uwsgi --http-socket :$PORT --wsgi-file main.py --callable app --master --processes 1 --threads 2 # [END gae_python3_custom_runtime] # [END gae_python38_custom_runtime] diff --git a/appengine/standard_python3/custom-server/requirements-test.txt b/appengine/standard_python3/custom-server/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/custom-server/requirements-test.txt +++ b/appengine/standard_python3/custom-server/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/custom-server/requirements.txt b/appengine/standard_python3/custom-server/requirements.txt index d3bc9fe5b15..11e0a1e7a23 100644 --- a/appengine/standard_python3/custom-server/requirements.txt +++ b/appengine/standard_python3/custom-server/requirements.txt @@ -1,2 +1,2 @@ uwsgi==2.0.22 -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' diff --git a/appengine/standard_python3/django/app.yaml b/appengine/standard_python3/django/app.yaml index 5a7255118c8..ddf86e23823 100644 --- a/appengine/standard_python3/django/app.yaml +++ b/appengine/standard_python3/django/app.yaml @@ -15,7 +15,7 @@ # # [START gaestd_py_django_app_yaml] -runtime: python39 +runtime: python313 env_variables: # This setting is used in settings.py to configure your ALLOWED_HOSTS diff --git a/appengine/standard_python3/django/noxfile_config.py b/appengine/standard_python3/django/noxfile_config.py index b05bde23ec6..ec3c324af4a 100644 --- a/appengine/standard_python3/django/noxfile_config.py +++ b/appengine/standard_python3/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", "3.8", "3.10", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "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/appengine/standard_python3/django/requirements-test.txt b/appengine/standard_python3/django/requirements-test.txt index 99bb7d24ca4..5279f42cfd4 100644 --- a/appengine/standard_python3/django/requirements-test.txt +++ b/appengine/standard_python3/django/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" pytest-django==4.5.0 diff --git a/appengine/standard_python3/django/requirements.txt b/appengine/standard_python3/django/requirements.txt index cdd4b54cf3e..13185477a1c 100644 --- a/appengine/standard_python3/django/requirements.txt +++ b/appengine/standard_python3/django/requirements.txt @@ -1,6 +1,4 @@ -Django==5.1.8; 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 +Django==6.0.5; python_version >= "3.12" +django-environ==0.13.0 psycopg2-binary==2.9.9 google-cloud-secret-manager==2.16.1 diff --git a/appengine/standard_python3/hello_world/requirements-test.txt b/appengine/standard_python3/hello_world/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/hello_world/requirements-test.txt +++ b/appengine/standard_python3/hello_world/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/hello_world/requirements.txt b/appengine/standard_python3/hello_world/requirements.txt index 047e9501aa8..b3ff32e568d 100644 --- a/appengine/standard_python3/hello_world/requirements.txt +++ b/appengine/standard_python3/hello_world/requirements.txt @@ -1 +1 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' diff --git a/appengine/standard_python3/migration/urlfetch/app.yaml b/appengine/standard_python3/migration/urlfetch/app.yaml index dd75aa47c69..3aa9d9d2207 100644 --- a/appengine/standard_python3/migration/urlfetch/app.yaml +++ b/appengine/standard_python3/migration/urlfetch/app.yaml @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 diff --git a/appengine/standard_python3/migration/urlfetch/requirements-test.txt b/appengine/standard_python3/migration/urlfetch/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/migration/urlfetch/requirements-test.txt +++ b/appengine/standard_python3/migration/urlfetch/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/migration/urlfetch/requirements.txt b/appengine/standard_python3/migration/urlfetch/requirements.txt index b366196b578..6b92818d794 100644 --- a/appengine/standard_python3/migration/urlfetch/requirements.txt +++ b/appengine/standard_python3/migration/urlfetch/requirements.txt @@ -1,3 +1,3 @@ -requests==2.28.2 -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' google-auth==2.17.3 diff --git a/appengine/standard_python3/pubsub/app.yaml b/appengine/standard_python3/pubsub/app.yaml index 9e3e948e4db..3c36b4bfb3c 100644 --- a/appengine/standard_python3/pubsub/app.yaml +++ b/appengine/standard_python3/pubsub/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 # [START gae_standard_pubsub_env] env_variables: diff --git a/appengine/standard_python3/pubsub/requirements-test.txt b/appengine/standard_python3/pubsub/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/pubsub/requirements-test.txt +++ b/appengine/standard_python3/pubsub/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/pubsub/requirements.txt b/appengine/standard_python3/pubsub/requirements.txt index 52d31b8c04e..067f794f801 100644 --- a/appengine/standard_python3/pubsub/requirements.txt +++ b/appengine/standard_python3/pubsub/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' google-api-python-client==2.86.0 google-auth==2.17.3 google-cloud-pubsub==2.16.0 diff --git a/appengine/standard_python3/redis/app.yaml b/appengine/standard_python3/redis/app.yaml index 2797ed154f7..138895c3737 100644 --- a/appengine/standard_python3/redis/app.yaml +++ b/appengine/standard_python3/redis/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 env_variables: REDIS_HOST: your-redis-host diff --git a/appengine/standard_python3/redis/requirements-test.txt b/appengine/standard_python3/redis/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/redis/requirements-test.txt +++ b/appengine/standard_python3/redis/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/redis/requirements.txt b/appengine/standard_python3/redis/requirements.txt index dddcdd089b7..f188e5dab47 100644 --- a/appengine/standard_python3/redis/requirements.txt +++ b/appengine/standard_python3/redis/requirements.txt @@ -1,2 +1,2 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' redis==4.5.4 diff --git a/appengine/standard_python3/spanner/app.yaml b/appengine/standard_python3/spanner/app.yaml index a4e3167ec08..59a31baca33 100644 --- a/appengine/standard_python3/spanner/app.yaml +++ b/appengine/standard_python3/spanner/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 env_variables: SPANNER_INSTANCE: "YOUR-SPANNER-INSTANCE-ID" diff --git a/appengine/standard_python3/spanner/requirements-test.txt b/appengine/standard_python3/spanner/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/spanner/requirements-test.txt +++ b/appengine/standard_python3/spanner/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/spanner/requirements.txt b/appengine/standard_python3/spanner/requirements.txt index 0bbdc887f99..73db7a71666 100644 --- a/appengine/standard_python3/spanner/requirements.txt +++ b/appengine/standard_python3/spanner/requirements.txt @@ -1,2 +1,2 @@ google-cloud-spanner==3.31.0 -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' diff --git a/appengine/standard_python3/warmup/app.yaml b/appengine/standard_python3/warmup/app.yaml index fdda19a79b1..3cc59533b01 100644 --- a/appengine/standard_python3/warmup/app.yaml +++ b/appengine/standard_python3/warmup/app.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -runtime: python39 +runtime: python313 inbound_services: - warmup diff --git a/appengine/standard_python3/warmup/requirements-test.txt b/appengine/standard_python3/warmup/requirements-test.txt index c2845bffbe8..c9e154ba440 100644 --- a/appengine/standard_python3/warmup/requirements-test.txt +++ b/appengine/standard_python3/warmup/requirements-test.txt @@ -1 +1 @@ -pytest==7.0.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/appengine/standard_python3/warmup/requirements.txt b/appengine/standard_python3/warmup/requirements.txt index 047e9501aa8..b3ff32e568d 100644 --- a/appengine/standard_python3/warmup/requirements.txt +++ b/appengine/standard_python3/warmup/requirements.txt @@ -1 +1 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' diff --git a/asset/snippets/noxfile_config.py b/asset/snippets/noxfile_config.py index 9a1680c88df..4eae59976e2 100644 --- a/asset/snippets/noxfile_config.py +++ b/asset/snippets/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.11"], + "ignored_versions": ["3.8", "3.9", "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/asset/snippets/requirements-test.txt b/asset/snippets/requirements-test.txt index d57b0bfd0ab..b85518d5117 100644 --- a/asset/snippets/requirements-test.txt +++ b/asset/snippets/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 flaky==3.8.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/auth/api-client/requirements-test.txt b/auth/api-client/requirements-test.txt index 6ff70adf77d..975d5ee58c2 100644 --- a/auth/api-client/requirements-test.txt +++ b/auth/api-client/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" backoff==2.2.1 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..658f73ff360 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": ["3.8", "3.9", "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, + "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..31d83c9e427 --- /dev/null +++ b/auth/cloud-client-temp/requirements.txt @@ -0,0 +1,7 @@ +google-cloud-compute==1.42.0 +google-cloud-storage==3.8.0 +google-auth==2.47.0 +pytest==9.0.3; python_version >= "3.10" +boto3>=1.26.0 +requests==2.34.2; python_version >= '3.10' +python-dotenv==1.2.2 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/cloud-client/requirements-test.txt b/auth/cloud-client/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/auth/cloud-client/requirements-test.txt +++ b/auth/cloud-client/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/auth/custom-credentials/aws/Dockerfile b/auth/custom-credentials/aws/Dockerfile new file mode 100644 index 00000000000..0cd34429f50 --- /dev/null +++ b/auth/custom-credentials/aws/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.14-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 71% rename from appengine/flexible_python37_and_earlier/hello_world_django/app.yaml rename to auth/custom-credentials/aws/noxfile_config.py index 62b74a9c27e..526763fbcc9 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,7 @@ # 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 = { + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], +} 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..8e23e776847 --- /dev/null +++ b/auth/custom-credentials/aws/requirements-test.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==9.0.3; python_version >= "3.10" diff --git a/auth/custom-credentials/aws/requirements.txt b/auth/custom-credentials/aws/requirements.txt new file mode 100644 index 00000000000..59b59103715 --- /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.2.2 +requests==2.34.2; python_version >= '3.10' 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/datastore/app.yaml b/auth/custom-credentials/okta/noxfile_config.py similarity index 80% rename from appengine/flexible_python37_and_earlier/datastore/app.yaml rename to auth/custom-credentials/okta/noxfile_config.py index ca76f83fc3b..379d24818b5 100644 --- a/appengine/flexible_python37_and_earlier/datastore/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": ["3.8", "3.9", "3.11", "3.12", "3.13"], +} diff --git a/auth/custom-credentials/okta/requirements-test.txt b/auth/custom-credentials/okta/requirements-test.txt new file mode 100644 index 00000000000..8e23e776847 --- /dev/null +++ b/auth/custom-credentials/okta/requirements-test.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==9.0.3; python_version >= "3.10" diff --git a/auth/custom-credentials/okta/requirements.txt b/auth/custom-credentials/okta/requirements.txt new file mode 100644 index 00000000000..95ba3242a1b --- /dev/null +++ b/auth/custom-credentials/okta/requirements.txt @@ -0,0 +1,4 @@ +requests==2.34.2; python_version >= '3.10' +google-cloud-storage==2.19.0 +google-auth==2.43.0 +python-dotenv==1.2.2 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/auth/downscoping/requirements-test.txt b/auth/downscoping/requirements-test.txt index 5d399275c93..a967ba62fb1 100644 --- a/auth/downscoping/requirements-test.txt +++ b/auth/downscoping/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-storage==2.9.0; python_version < '3.7' google-cloud-storage==2.9.0; python_version > '3.6' diff --git a/auth/end-user/web/requirements-test.txt b/auth/end-user/web/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/auth/end-user/web/requirements-test.txt +++ b/auth/end-user/web/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/auth/end-user/web/requirements.txt b/auth/end-user/web/requirements.txt index f40ba1c62a4..2f317ce73b5 100644 --- a/auth/end-user/web/requirements.txt +++ b/auth/end-user/web/requirements.txt @@ -2,5 +2,5 @@ google-auth==2.38.0 google-auth-oauthlib==1.2.1 google-auth-httplib2==0.2.0 google-api-python-client==2.131.0 -flask==3.0.3 -requests==2.31.0 +Flask==3.1.3; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' diff --git a/auth/service-to-service/noxfile_config.py b/auth/service-to-service/noxfile_config.py index bf45881273d..f6a3cf7417f 100644 --- a/auth/service-to-service/noxfile_config.py +++ b/auth/service-to-service/noxfile_config.py @@ -23,7 +23,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. # We only run the cloud run tests in py38 session. - "ignored_versions": ["2.7", "3.6", "3.7"], + "ignored_versions": ["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/auth/service-to-service/requirements-test.txt b/auth/service-to-service/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/auth/service-to-service/requirements-test.txt +++ b/auth/service-to-service/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/auth/service-to-service/requirements.txt b/auth/service-to-service/requirements.txt index ece414abb35..0bf168d9f10 100644 --- a/auth/service-to-service/requirements.txt +++ b/auth/service-to-service/requirements.txt @@ -1,2 +1,2 @@ google-auth==2.19.1 -requests==2.32.4 +requests==2.34.2; python_version >= '3.10' diff --git a/automl/snippets/noxfile_config.py b/automl/snippets/noxfile_config.py index ef111e5e309..b4a45d95757 100644 --- a/automl/snippets/noxfile_config.py +++ b/automl/snippets/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.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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/automl/snippets/requirements-test.txt b/automl/snippets/requirements-test.txt index f3230681cda..79932f83530 100644 --- a/automl/snippets/requirements-test.txt +++ b/automl/snippets/requirements-test.txt @@ -1,2 +1,2 @@ backoff==2.2.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/batch/requirements-test.txt b/batch/requirements-test.txt index 08d1b1b9c1f..4daf4a01072 100644 --- a/batch/requirements-test.txt +++ b/batch/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-compute==1.11.0 google-cloud-resource-manager==1.10.1 google-cloud-storage==2.9.0 diff --git a/bigquery-connection/snippets/noxfile_config.py b/bigquery-connection/snippets/noxfile_config.py index 28c09af52f7..6cd93d1263d 100644 --- a/bigquery-connection/snippets/noxfile_config.py +++ b/bigquery-connection/snippets/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.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery-connection/snippets/requirements-test.txt b/bigquery-connection/snippets/requirements-test.txt index 5b0f38d50e2..334e1bfdbb8 100644 --- a/bigquery-connection/snippets/requirements-test.txt +++ b/bigquery-connection/snippets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-testutils==1.5.0 \ No newline at end of file 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/bigquery-datatransfer/snippets/noxfile_config.py b/bigquery-datatransfer/snippets/noxfile_config.py index 161ffcc14f3..5c1c0aa7b2f 100644 --- a/bigquery-datatransfer/snippets/noxfile_config.py +++ b/bigquery-datatransfer/snippets/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.11"], + "ignored_versions": ["3.8", "3.9", "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/bigquery-datatransfer/snippets/requirements-test.txt b/bigquery-datatransfer/snippets/requirements-test.txt index ae8913096ea..3c6ff1b456d 100644 --- a/bigquery-datatransfer/snippets/requirements-test.txt +++ b/bigquery-datatransfer/snippets/requirements-test.txt @@ -1,4 +1,4 @@ google-cloud-bigquery==3.27.0 google-cloud-pubsub==2.28.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" mock==5.1.0 diff --git a/bigquery-migration/snippets/noxfile_config.py b/bigquery-migration/snippets/noxfile_config.py index 68825a3b2dc..5ed96a67651 100644 --- a/bigquery-migration/snippets/noxfile_config.py +++ b/bigquery-migration/snippets/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.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery-migration/snippets/requirements-test.txt b/bigquery-migration/snippets/requirements-test.txt index d54b3ea50e2..6b60bac71d7 100644 --- a/bigquery-migration/snippets/requirements-test.txt +++ b/bigquery-migration/snippets/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-testutils==1.5.0 google-api-core==2.17.1 google-cloud-storage==2.9.0 \ No newline at end of file diff --git a/bigquery-reservation/snippets/noxfile_config.py b/bigquery-reservation/snippets/noxfile_config.py index e1c631ca86f..4fb2be93998 100644 --- a/bigquery-reservation/snippets/noxfile_config.py +++ b/bigquery-reservation/snippets/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.6"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery-reservation/snippets/requirements-test.txt b/bigquery-reservation/snippets/requirements-test.txt index 840c3fcffe5..3e1319f761b 100644 --- a/bigquery-reservation/snippets/requirements-test.txt +++ b/bigquery-reservation/snippets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-testutils==1.5.0 diff --git a/bigquery/bigframes/.gitignore b/bigquery/bigframes/.gitignore new file mode 100644 index 00000000000..6e1f113ef05 --- /dev/null +++ b/bigquery/bigframes/.gitignore @@ -0,0 +1 @@ +noxfile.py diff --git a/bigquery/bigframes/README.md b/bigquery/bigframes/README.md new file mode 100644 index 00000000000..a243089ef7f --- /dev/null +++ b/bigquery/bigframes/README.md @@ -0,0 +1,10 @@ +# BigQuery DataFrames code samples + +This directory contains code samples for [BigQuery DataFrames (aka +BigFrames)](https://dataframes.bigquery.dev/). + +To install BigQuery DataFrames, run: + +``` +pip install --upgrade bigframes +``` diff --git a/bigquery/bigframes/call_python_udf.py b/bigquery/bigframes/call_python_udf.py new file mode 100644 index 00000000000..200b1c1fb54 --- /dev/null +++ b/bigquery/bigframes/call_python_udf.py @@ -0,0 +1,105 @@ +# 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 bigquery_dataframes_call_python_udf] +import textwrap +from typing import Tuple + +import bigframes.pandas as bpd +import pandas as pd +import pyarrow as pa + + +# Using partial ordering mode enables more efficient query optimizations. +bpd.options.bigquery.ordering_mode = "partial" + + +def call_python_udf( + project_id: str, location: str, +) -> Tuple[pd.Series, bpd.Series]: + # Set the billing project to use for queries. This step is optional, as the + # project can be inferred from your environment in many cases. + bpd.options.bigquery.project = project_id # "your-project-id" + + # Since this example works with local data, set a processing location. + bpd.options.bigquery.location = location # "US" + + # Create a sample series. + xml_series = pd.Series( + [ + textwrap.dedent( + """ + + The Great Gatsby + F. Scott Fitzgerald + + """ + ), + textwrap.dedent( + """ + + 1984 + George Orwell + + """ + ), + textwrap.dedent( + """ + + Brave New World + Aldous Huxley + + """ + ), + ], + dtype=pd.ArrowDtype(pa.string()), + ) + df = pd.DataFrame({"xml": xml_series}) + + # Use the BigQuery Accessor, which is automatically registered on pandas + # DataFrames when you import bigframes. This example uses a function that + # has been deployed to bigquery-utils for demonstration purposes. To use in + # production, deploy the function at + # https://github.com/GoogleCloudPlatform/bigquery-utils/blob/master/udfs/community/cw_xml_extract.sqlx + # to your own project. + titles_pandas = df.bigquery.sql_scalar( + "`bqutil`.`fn`.cw_xml_extract({xml}, '//title/text()')", + ) + + # Alternatively, call read_gbq_function to get a pointer to the function + # that can be applied on BigQuery DataFrames objects. + cw_xml_extract = bpd.read_gbq_function("bqutil.fn.cw_xml_extract") + xml_bigframes = bpd.read_pandas(xml_series) + + xpath_query = "//title/text()" + titles_bigframes = xml_bigframes.apply(cw_xml_extract, args=(xpath_query,)) + return titles_pandas, titles_bigframes + # [END bigquery_dataframes_call_python_udf] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + + # Note: GCP project ID can be inferred from the environment if Application + # Default Credentials are set, so None is perfectly valid for --project_id. + parser.add_argument("--project_id", type=str) + parser.add_argument("--location", default="US", type=str) + args = parser.parse_args() + + pddf, bfdf = call_python_udf(project_id=args.project_id, location=args.location) + print(pddf) + print(bfdf.to_pandas()) diff --git a/appengine/flexible_python37_and_earlier/hello_world/main_test.py b/bigquery/bigframes/call_python_udf_test.py similarity index 61% rename from appengine/flexible_python37_and_earlier/hello_world/main_test.py rename to bigquery/bigframes/call_python_udf_test.py index a6049b094f9..67475d94718 100644 --- a/appengine/flexible_python37_and_earlier/hello_world/main_test.py +++ b/bigquery/bigframes/call_python_udf_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 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. @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import main +import bigframes.pandas as bpd +import call_python_udf -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") +def test_call_python_udf(project_id: str, location: str) -> None: + bpd.close_session() + pd_result, bf_result = call_python_udf.call_python_udf(project_id=project_id, location=location) + assert len(pd_result.index) == 3 + assert len(bf_result.index) == 3 diff --git a/bigquery/bigframes/conftest.py b/bigquery/bigframes/conftest.py new file mode 100644 index 00000000000..66558b11552 --- /dev/null +++ b/bigquery/bigframes/conftest.py @@ -0,0 +1,40 @@ +# 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 +# +# 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 uuid + +from google.cloud import bigquery +import pytest + + +@pytest.fixture(scope="session") +def project_id() -> str: + return os.environ["GOOGLE_CLOUD_PROJECT"] + + +@pytest.fixture(scope="session") +def location() -> str: + return "US" + + +@pytest.fixture(scope="session") +def dataset_id(project_id: str) -> str: + client = bigquery.Client(project=project_id) + dataset_id = f"bigframes_samples_{uuid.uuid4().hex[:8]}" + dataset = bigquery.Dataset(f"{project_id}.{dataset_id}") + dataset.location = "US" + client.create_dataset(dataset) + yield dataset_id + client.delete_dataset(dataset, delete_contents=True, not_found_ok=True) diff --git a/appengine/flexible_python37_and_earlier/hello_world_django/noxfile_config.py b/bigquery/bigframes/noxfile_config.py similarity index 88% rename from appengine/flexible_python37_and_earlier/hello_world_django/noxfile_config.py rename to bigquery/bigframes/noxfile_config.py index 1665dd736f8..322efba33e8 100644 --- a/appengine/flexible_python37_and_earlier/hello_world_django/noxfile_config.py +++ b/bigquery/bigframes/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2023 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. @@ -22,11 +22,10 @@ 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": ["3.8", "3.9", "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, + "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 diff --git a/bigquery/bigframes/query_bqstorage.py b/bigquery/bigframes/query_bqstorage.py new file mode 100644 index 00000000000..02dc6b0f323 --- /dev/null +++ b/bigquery/bigframes/query_bqstorage.py @@ -0,0 +1,44 @@ +# 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 bigquery_bigframes_query_bqstorage] +import bigframes.pandas as bpd + +import pandas as pd + +# Set partial ordering mode as the default configuration for BigQuery +# DataFrames. +bpd.options.bigquery.ordering_mode = "partial" + + +def query_bqstorage() -> pd.DataFrame: + sql = """ + SELECT name FROM `bigquery-public-data.usa_names.usa_1910_current` + WHERE state = 'TX' + LIMIT 100 + """ + + # Read query results into a server-side DataFrame without downloading data. + df = bpd.read_gbq(sql) + + # When downloading results to an in-memory pandas DataFrame, + # bigquery-dataframes automatically uses the BigQuery Storage API if + # installed. + pandas_df = df.to_pandas() + return pandas_df +# [END bigquery_bigframes_query_bqstorage] + + +if __name__ == "__main__": + print(query_bqstorage()) diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/views.py b/bigquery/bigframes/query_bqstorage_test.py similarity index 76% rename from appengine/flexible_python37_and_earlier/django_cloudsql/polls/views.py rename to bigquery/bigframes/query_bqstorage_test.py index 262f571d568..d02d858da30 100644 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/views.py +++ b/bigquery/bigframes/query_bqstorage_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 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. @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.http import HttpResponse +import query_bqstorage -def index(request): - return HttpResponse("Hello, world. You're at the polls index.") +def test_query_bqstorage() -> None: + pandas_df = query_bqstorage.query_bqstorage() + assert pandas_df is not None diff --git a/bigquery/bigframes/query_parameters.py b/bigquery/bigframes/query_parameters.py new file mode 100644 index 00000000000..557fb5f72d7 --- /dev/null +++ b/bigquery/bigframes/query_parameters.py @@ -0,0 +1,49 @@ +# 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 bigquery_bigframes_query_parameters] +import bigframes.pandas as bpd + +# Set partial ordering mode as the default configuration for BigQuery +# DataFrames. +bpd.options.bigquery.ordering_mode = "partial" + + +def query_parameters() -> bpd.DataFrame: + sql = """ + SELECT name FROM `bigquery-public-data.usa_names.usa_1910_current` + WHERE state = @state + LIMIT 100 + """ + + query_config = { + "query": { + "parameterMode": "NAMED", + "queryParameters": [ + { + "name": "state", + "parameterType": {"type": "STRING"}, + "parameterValue": {"value": "TX"}, + } + ], + } + } + + df = bpd.read_gbq(sql, configuration=query_config) + return df +# [END bigquery_bigframes_query_parameters] + + +if __name__ == "__main__": + print(query_parameters()) diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/urls.py b/bigquery/bigframes/query_parameters_test.py similarity index 77% rename from appengine/flexible_python37_and_earlier/django_cloudsql/polls/urls.py rename to bigquery/bigframes/query_parameters_test.py index ca52d749043..e6be1c21424 100644 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/polls/urls.py +++ b/bigquery/bigframes/query_parameters_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 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. @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from django.urls import path +import query_parameters -from . import views -urlpatterns = [ - path("", views.index, name="index"), -] +def test_query_parameters() -> None: + df = query_parameters.query_parameters() + assert df is not None diff --git a/bigquery/bigframes/query_standard_sql.py b/bigquery/bigframes/query_standard_sql.py new file mode 100644 index 00000000000..adee2b05845 --- /dev/null +++ b/bigquery/bigframes/query_standard_sql.py @@ -0,0 +1,46 @@ +# 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 bigquery_bigframes_query] +import bigframes.pandas as bpd + +# Set partial ordering mode as the default configuration for BigQuery +# DataFrames. +bpd.options.bigquery.ordering_mode = "partial" + + +def query_standard_sql(project_id: str = "your-project-id") -> bpd.DataFrame: + sql = """ + SELECT name FROM `bigquery-public-data.usa_names.usa_1910_current` + WHERE state = 'TX' + LIMIT 100 + """ + + # Run a query alongside existing SQL. The project will be determined from + # default credentials. + df = bpd.read_gbq(sql) + + # Run a query after explicitly specifying a project. + bpd.close_session() + bpd.options.bigquery.project = project_id + df = bpd.read_gbq(sql) + return df +# [END bigquery_bigframes_query] + + +if __name__ == "__main__": + import os + + project = os.environ.get("GOOGLE_CLOUD_PROJECT", "your-project-id") + print(query_standard_sql(project_id=project)) diff --git a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/wsgi.py b/bigquery/bigframes/query_standard_sql_test.py similarity index 72% rename from appengine/flexible_python37_and_earlier/django_cloudsql/mysite/wsgi.py rename to bigquery/bigframes/query_standard_sql_test.py index 968cf994b60..a4ba779220e 100644 --- a/appengine/flexible_python37_and_earlier/django_cloudsql/mysite/wsgi.py +++ b/bigquery/bigframes/query_standard_sql_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 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. @@ -12,11 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import query_standard_sql -import os -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") - -application = get_wsgi_application() +def test_query_standard_sql(project_id: str) -> None: + df = query_standard_sql.query_standard_sql(project_id=project_id) + assert df is not None diff --git a/bigquery/bigframes/requirements-test.txt b/bigquery/bigframes/requirements-test.txt new file mode 100644 index 00000000000..8b8e28c484c --- /dev/null +++ b/bigquery/bigframes/requirements-test.txt @@ -0,0 +1,2 @@ +flaky==3.8.1 +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/bigframes/requirements.txt b/bigquery/bigframes/requirements.txt new file mode 100644 index 00000000000..a14856a58d8 --- /dev/null +++ b/bigquery/bigframes/requirements.txt @@ -0,0 +1 @@ +bigframes==2.38.0 diff --git a/bigquery/bigframes/upload_from_dataframe.py b/bigquery/bigframes/upload_from_dataframe.py new file mode 100644 index 00000000000..4f878d02add --- /dev/null +++ b/bigquery/bigframes/upload_from_dataframe.py @@ -0,0 +1,52 @@ +# 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 bigquery_bigframes_upload_from_dataframe] +import bigframes.pandas as bpd + +import pandas as pd + +# Set partial ordering mode as the default configuration for BigQuery +# DataFrames. +bpd.options.bigquery.ordering_mode = "partial" + + +def upload_from_dataframe( + table_id: str = "your-project.your_dataset.your_table_name", +) -> bpd.DataFrame: + # Create a local pandas DataFrame. + df = pd.DataFrame( + { + "my_string": ["a", "b", "c"], + "my_int64": [1, 2, 3], + "my_float64": [4.0, 5.0, 6.0], + } + ) + + # Convert the local pandas DataFrame to a BigQuery DataFrame. + bq_df = bpd.read_pandas(df) + + # Write the DataFrame to a BigQuery table. + bq_df.to_gbq(table_id, if_exists="replace") + return bq_df +# [END bigquery_bigframes_upload_from_dataframe] + + +if __name__ == "__main__": + import os + + table_id = os.environ.get( + "TABLE_ID", "your-project.your_dataset.your_table_name" + ) + print(upload_from_dataframe(table_id=table_id)) diff --git a/appengine/flexible_python37_and_earlier/datastore/main_test.py b/bigquery/bigframes/upload_from_dataframe_test.py similarity index 64% rename from appengine/flexible_python37_and_earlier/datastore/main_test.py rename to bigquery/bigframes/upload_from_dataframe_test.py index 6b17c44ca79..799e52618a0 100644 --- a/appengine/flexible_python37_and_earlier/datastore/main_test.py +++ b/bigquery/bigframes/upload_from_dataframe_test.py @@ -1,4 +1,4 @@ -# Copyright 2015 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. @@ -12,13 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import main +import upload_from_dataframe -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") +def test_upload_from_dataframe(project_id: str, dataset_id: str) -> None: + table_id = f"{project_id}.{dataset_id}.upload_from_dataframe" + bq_df = upload_from_dataframe.upload_from_dataframe(table_id=table_id) + assert bq_df is not None diff --git a/bigquery/bqml/noxfile_config.py b/bigquery/bqml/noxfile_config.py index 4e2c703470f..a4cb0cac197 100644 --- a/bigquery/bqml/noxfile_config.py +++ b/bigquery/bqml/noxfile_config.py @@ -23,7 +23,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.6", "3.9", "3.11"], + "ignored_versions": ["3.8", "3.9", "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/bigquery/bqml/requirements-test.txt b/bigquery/bqml/requirements-test.txt index f1684cd8061..8b8e28c484c 100644 --- a/bigquery/bqml/requirements-test.txt +++ b/bigquery/bqml/requirements-test.txt @@ -1,2 +1,2 @@ flaky==3.8.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/bqml/requirements.txt b/bigquery/bqml/requirements.txt index cfed3976b1d..c511b62074d 100644 --- a/bigquery/bqml/requirements.txt +++ b/bigquery/bqml/requirements.txt @@ -2,7 +2,6 @@ google-cloud-bigquery[pandas,bqstorage]==3.27.0 google-cloud-bigquery-storage==2.27.0 pandas==2.0.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pyarrow==17.0.0; python_version <= '3.8' -pyarrow==20.0.0; python_version > '3.9' +pyarrow==24.0.0 flaky==3.8.1 mock==5.1.0 diff --git a/bigquery/cloud-client/requirements-test.txt b/bigquery/cloud-client/requirements-test.txt index 7d32dfc20c7..1fb857b4d88 100644 --- a/bigquery/cloud-client/requirements-test.txt +++ b/bigquery/cloud-client/requirements-test.txt @@ -1,3 +1,3 @@ # samples/snippets should be runnable with no "extras" google-cloud-testutils==1.5.0 -pytest==8.3.4 +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/continuous-queries/requirements-test.txt b/bigquery/continuous-queries/requirements-test.txt index ecdd071f48d..5c7507d293a 100644 --- a/bigquery/continuous-queries/requirements-test.txt +++ b/bigquery/continuous-queries/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.3.5 +pytest==9.0.3; python_version >= "3.10" google-auth==2.38.0 -requests==2.32.4 +requests==2.34.2; python_version >= '3.10' diff --git a/bigquery/continuous-queries/requirements.txt b/bigquery/continuous-queries/requirements.txt index 244b3dea27d..1ef1c4d1c6a 100644 --- a/bigquery/continuous-queries/requirements.txt +++ b/bigquery/continuous-queries/requirements.txt @@ -1,4 +1,4 @@ functions-framework==3.9.2 google-cloud-bigquery==3.30.0 google-auth==2.38.0 -requests==2.32.4 +requests==2.34.2; python_version >= '3.10' diff --git a/bigquery/pandas-gbq-migration/noxfile_config.py b/bigquery/pandas-gbq-migration/noxfile_config.py index 4e2c703470f..a4cb0cac197 100644 --- a/bigquery/pandas-gbq-migration/noxfile_config.py +++ b/bigquery/pandas-gbq-migration/noxfile_config.py @@ -23,7 +23,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.6", "3.9", "3.11"], + "ignored_versions": ["3.8", "3.9", "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/bigquery/pandas-gbq-migration/requirements-test.txt b/bigquery/pandas-gbq-migration/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/bigquery/pandas-gbq-migration/requirements-test.txt +++ b/bigquery/pandas-gbq-migration/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/pandas-gbq-migration/requirements.txt b/bigquery/pandas-gbq-migration/requirements.txt index 2e8f1a6e66d..cec31109491 100644 --- a/bigquery/pandas-gbq-migration/requirements.txt +++ b/bigquery/pandas-gbq-migration/requirements.txt @@ -5,5 +5,4 @@ pandas==2.2.3; python_version > '3.8' pandas-gbq==0.24.0 grpcio==1.70.0; python_version == '3.8' grpcio==1.74.0; python_version > '3.8' -pyarrow==17.0.0; python_version <= '3.8' -pyarrow==20.0.0; python_version > '3.9' +pyarrow==24.0.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/generative_ai/labels/noxfile_config.py b/bigquery/python-db-dtypes-pandas/snippets/noxconfig.py similarity index 96% rename from generative_ai/labels/noxfile_config.py rename to bigquery/python-db-dtypes-pandas/snippets/noxconfig.py index 962ba40a926..b9d835eefee 100644 --- a/generative_ai/labels/noxfile_config.py +++ b/bigquery/python-db-dtypes-pandas/snippets/noxconfig.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.11", "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, 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..c9e154ba440 --- /dev/null +++ b/bigquery/python-db-dtypes-pandas/snippets/requirements-test.txt @@ -0,0 +1 @@ +pytest==9.0.3; python_version >= "3.10" 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/bigquery/remote-function/document/noxfile_config.py b/bigquery/remote-function/document/noxfile_config.py index 129472ab778..05254a9bf8a 100644 --- a/bigquery/remote-function/document/noxfile_config.py +++ b/bigquery/remote-function/document/noxfile_config.py @@ -17,7 +17,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.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery/remote-function/document/requirements-test.txt b/bigquery/remote-function/document/requirements-test.txt index 254febb7aba..87181ecea77 100644 --- a/bigquery/remote-function/document/requirements-test.txt +++ b/bigquery/remote-function/document/requirements-test.txt @@ -1,4 +1,4 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-documentai==3.0.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/remote-function/document/requirements.txt b/bigquery/remote-function/document/requirements.txt index 5d039df280e..8d5a7413139 100644 --- a/bigquery/remote-function/document/requirements.txt +++ b/bigquery/remote-function/document/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-documentai==3.0.1 -Werkzeug==2.3.8 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/bigquery/remote-function/translate/noxfile_config.py b/bigquery/remote-function/translate/noxfile_config.py index 881bc58580f..45b90dd8081 100644 --- a/bigquery/remote-function/translate/noxfile_config.py +++ b/bigquery/remote-function/translate/noxfile_config.py @@ -17,7 +17,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.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery/remote-function/translate/requirements-test.txt b/bigquery/remote-function/translate/requirements-test.txt index 2048a36731f..d5b31aa84f7 100644 --- a/bigquery/remote-function/translate/requirements-test.txt +++ b/bigquery/remote-function/translate/requirements-test.txt @@ -1,4 +1,4 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-translate==3.18.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/remote-function/translate/requirements.txt b/bigquery/remote-function/translate/requirements.txt index 8f3760f3846..3608debb3e3 100644 --- a/bigquery/remote-function/translate/requirements.txt +++ b/bigquery/remote-function/translate/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-translate==3.18.0 -Werkzeug==2.3.8 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/bigquery/remote-function/vision/noxfile_config.py b/bigquery/remote-function/vision/noxfile_config.py index 881bc58580f..45b90dd8081 100644 --- a/bigquery/remote-function/vision/noxfile_config.py +++ b/bigquery/remote-function/vision/noxfile_config.py @@ -17,7 +17,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.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery/remote-function/vision/requirements-test.txt b/bigquery/remote-function/vision/requirements-test.txt index 62634fcffc0..be97aeae79f 100644 --- a/bigquery/remote-function/vision/requirements-test.txt +++ b/bigquery/remote-function/vision/requirements-test.txt @@ -1,4 +1,4 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-vision==3.8.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/remote-function/vision/requirements.txt b/bigquery/remote-function/vision/requirements.txt index 6737756c476..f84e430b9b4 100644 --- a/bigquery/remote-function/vision/requirements.txt +++ b/bigquery/remote-function/vision/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-vision==3.8.1 -Werkzeug==2.3.8 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/bigquery_storage/pyarrow/noxfile_config.py b/bigquery_storage/pyarrow/noxfile_config.py index 29edb31ffe8..fea7f946dff 100644 --- a/bigquery_storage/pyarrow/noxfile_config.py +++ b/bigquery_storage/pyarrow/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery_storage/pyarrow/requirements-test.txt b/bigquery_storage/pyarrow/requirements-test.txt index 7561ed55ce2..c9e154ba440 100644 --- a/bigquery_storage/pyarrow/requirements-test.txt +++ b/bigquery_storage/pyarrow/requirements-test.txt @@ -1,3 +1 @@ -pytest===7.4.3; python_version == '3.7' -pytest===8.3.5; python_version == '3.8' -pytest==8.4.1; python_version >= '3.9' +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery_storage/quickstart/noxfile_config.py b/bigquery_storage/quickstart/noxfile_config.py index f1fa9e5618b..0973c8621c7 100644 --- a/bigquery_storage/quickstart/noxfile_config.py +++ b/bigquery_storage/quickstart/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery_storage/quickstart/requirements-test.txt b/bigquery_storage/quickstart/requirements-test.txt index 7561ed55ce2..c9e154ba440 100644 --- a/bigquery_storage/quickstart/requirements-test.txt +++ b/bigquery_storage/quickstart/requirements-test.txt @@ -1,3 +1 @@ -pytest===7.4.3; python_version == '3.7' -pytest===8.3.5; python_version == '3.8' -pytest==8.4.1; python_version >= '3.9' +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery_storage/quickstart/v1beta1-migration-guide.md b/bigquery_storage/quickstart/v1beta1-migration-guide.md new file mode 100644 index 00000000000..b57a91db841 --- /dev/null +++ b/bigquery_storage/quickstart/v1beta1-migration-guide.md @@ -0,0 +1,124 @@ +# Migrating BigQuery Storage API from v1beta1 to v1: Python + +This guide shows how to migrate Python code using the BigQuery Storage API from +version `v1beta1` to `v1`. + +## Key Changes + +* **Package Imports**: `google.cloud.bigquery_storage_v1beta1` -> + `google.cloud.bigquery_storage_v1` +* **Service Client**: `BigQueryStorageClient` is replaced by + `BigQueryReadClient`. +* **Table Reference**: `TableReference` type is replaced by a simple string + representation of the table path in `ReadSession.table`. +* **Session Configuration**: Configuration fields (table, format, read + options) have moved into `ReadSession` type, which is passed in + `CreateReadSessionRequest`. +* **Parallelism**: `requested_streams` parameter in `create_read_session` is + replaced by `max_stream_count`. +* **Sharding Strategy**: `sharding_strategy` is removed. The server now + automatically balances the streams. +* **Read Rows Request**: `StreamPosition` is flattened. You now pass the + stream name directly to `read_rows`. + +## Code Comparison + +### 1. Client Initialization + +**v1beta1:** + +```python +from google.cloud import bigquery_storage_v1beta1 + +client = bigquery_storage_v1beta1.BigQueryStorageClient() +``` + +**v1:** + +```python +from google.cloud import bigquery_storage_v1 + +client = bigquery_storage_v1.BigQueryReadClient() +``` + +### 2. Creating a Read Session + +**v1beta1:** + +```python +from google.cloud import bigquery_storage_v1beta1 + +table_ref = bigquery_storage_v1beta1.types.TableReference() +table_ref.project_id = "bigquery-public-data" +table_ref.dataset_id = "usa_names" +table_ref.table_id = "usa_1910_current" + +read_options = bigquery_storage_v1beta1.types.TableReadOptions() +read_options.selected_fields.append("name") +read_options.row_restriction = 'state = "WA"' + +session = client.create_read_session( + table_reference=table_ref, + parent='projects/read-session-project', + read_options=read_options, + requested_streams=1, + format=bigquery_storage_v1beta1.types.DataFormat.AVRO, + sharding_strategy=bigquery_storage_v1beta1.types.ShardingStrategy.LIQUID +) +``` + +**v1:** + +```python +from google.cloud import bigquery_storage_v1 + +# Table path is now a string: projects/{project}/datasets/{dataset}/tables/{table} +table_path = "projects/bigquery-public-data/datasets/usa_names/tables/usa_1910_current" + +read_options = bigquery_storage_v1.types.ReadSession.TableReadOptions() +read_options.selected_fields.append("name") +read_options.row_restriction = 'state = "WA"' + +# ReadSession holds the session configuration +read_session = bigquery_storage_v1.types.ReadSession( + table=table_path, + data_format=bigquery_storage_v1.types.DataFormat.AVRO, # format renamed to data_format + read_options=read_options +) + +session = client.create_read_session( + parent="projects/read-session-project", + read_session=read_session, + max_stream_count=1 # requested_streams renamed to max_stream_count +) +``` + +### 3. Reading Rows + +**v1beta1:** + +```python +from google.cloud import bigquery_storage_v1beta1 + +position = bigquery_storage_v1beta1.types.StreamPosition( + stream=session.streams[0] +) + +reader = client.read_rows(read_position=position) + +for row in reader.rows(session): + print(row["name"]) +``` + +**v1:** + +```python +# read_rows accepts stream name and offset directly. +# Note that the parameter name in the library helper is 'name', which maps to 'read_stream' in proto. +reader = client.read_rows(session.streams[0].name) + +# In v1, you don't need to pass session to rows() if schema info is already present, +# but the client library handles it. +for row in reader.rows(): + print(row["name"]) +``` diff --git a/bigquery_storage/snippets/noxfile_config.py b/bigquery_storage/snippets/noxfile_config.py index f1fa9e5618b..0973c8621c7 100644 --- a/bigquery_storage/snippets/noxfile_config.py +++ b/bigquery_storage/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery_storage/snippets/requirements-test.txt b/bigquery_storage/snippets/requirements-test.txt index 230ca56dc3a..06cc720170b 100644 --- a/bigquery_storage/snippets/requirements-test.txt +++ b/bigquery_storage/snippets/requirements-test.txt @@ -1,4 +1,2 @@ google-cloud-testutils==1.6.4 -pytest===7.4.3; python_version == '3.7' -pytest===8.3.5; python_version == '3.8' -pytest==8.4.1; python_version >= '3.9' +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery_storage/snippets/requirements.txt b/bigquery_storage/snippets/requirements.txt index 8a456493526..821c0abb139 100644 --- a/bigquery_storage/snippets/requirements.txt +++ b/bigquery_storage/snippets/requirements.txt @@ -1,6 +1,4 @@ google-cloud-bigquery-storage==2.32.0 google-cloud-bigquery===3.30.0; python_version <= '3.8' google-cloud-bigquery==3.35.1; python_version >= '3.9' -pytest===7.4.3; python_version == '3.7' -pytest===8.3.5; python_version == '3.8' -pytest==8.4.1; python_version >= '3.9' +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery_storage/to_dataframe/noxfile_config.py b/bigquery_storage/to_dataframe/noxfile_config.py index f1fa9e5618b..0973c8621c7 100644 --- a/bigquery_storage/to_dataframe/noxfile_config.py +++ b/bigquery_storage/to_dataframe/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/bigquery_storage/to_dataframe/requirements-test.txt b/bigquery_storage/to_dataframe/requirements-test.txt index 7561ed55ce2..c9e154ba440 100644 --- a/bigquery_storage/to_dataframe/requirements-test.txt +++ b/bigquery_storage/to_dataframe/requirements-test.txt @@ -1,3 +1 @@ -pytest===7.4.3; python_version == '3.7' -pytest===8.3.5; python_version == '3.8' -pytest==8.4.1; python_version >= '3.9' +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery_storage/to_dataframe/requirements.txt b/bigquery_storage/to_dataframe/requirements.txt index e3b75fdaf5f..6ab84896a61 100644 --- a/bigquery_storage/to_dataframe/requirements.txt +++ b/bigquery_storage/to_dataframe/requirements.txt @@ -1,19 +1,17 @@ google-auth==2.40.3 google-cloud-bigquery-storage==2.32.0 -google-cloud-bigquery===3.30.0; python_version <= '3.8' +google-cloud-bigquery==3.30.0; python_version <= '3.8' google-cloud-bigquery==3.35.1; python_version >= '3.9' -pyarrow===12.0.1; python_version == '3.7' -pyarrow===17.0.0; python_version == '3.8' -pyarrow==21.0.0; python_version >= '3.9' -ipython===7.31.1; python_version == '3.7' -ipython===8.10.0; python_version == '3.8' -ipython===8.18.1; python_version == '3.9' -ipython===8.33.0; python_version == '3.10' +pyarrow==24.0.0 +ipython==7.31.1; python_version == '3.7' +ipython==8.10.0; python_version == '3.8' +ipython==8.18.1; python_version == '3.9' +ipython==8.33.0; python_version == '3.10' ipython==9.4.0; python_version >= '3.11' ipywidgets==8.1.7 -pandas===1.3.5; python_version == '3.7' -pandas===2.0.3; python_version == '3.8' +pandas==1.3.5; python_version == '3.7' +pandas==2.0.3; python_version == '3.8' pandas==2.3.1; python_version >= '3.9' tqdm==4.67.1 -db-dtypes===1.4.2; python_version <= '3.8' +db-dtypes==1.4.2; python_version <= '3.8' db-dtypes==1.4.3; python_version >= '3.9' diff --git a/billing/requirements-test.txt b/billing/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/billing/requirements-test.txt +++ b/billing/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/blog/introduction_to_data_models_in_cloud_datastore/noxfile_config.py b/blog/introduction_to_data_models_in_cloud_datastore/noxfile_config.py index 9a1680c88df..4eae59976e2 100644 --- a/blog/introduction_to_data_models_in_cloud_datastore/noxfile_config.py +++ b/blog/introduction_to_data_models_in_cloud_datastore/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.11"], + "ignored_versions": ["3.8", "3.9", "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/blog/introduction_to_data_models_in_cloud_datastore/requirements-test.txt b/blog/introduction_to_data_models_in_cloud_datastore/requirements-test.txt index 185d62c4204..23df1e03c7e 100644 --- a/blog/introduction_to_data_models_in_cloud_datastore/requirements-test.txt +++ b/blog/introduction_to_data_models_in_cloud_datastore/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 diff --git a/cdn/requirements-test.txt b/cdn/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cdn/requirements-test.txt +++ b/cdn/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud-media-livestream/keypublisher/noxfile_config.py b/cloud-media-livestream/keypublisher/noxfile_config.py index 3f0b74f9b96..5659ab2853c 100644 --- a/cloud-media-livestream/keypublisher/noxfile_config.py +++ b/cloud-media-livestream/keypublisher/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.9", "3.10", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/cloud-media-livestream/keypublisher/requirements.txt b/cloud-media-livestream/keypublisher/requirements.txt index f56357f0f87..b0e56a89ea7 100644 --- a/cloud-media-livestream/keypublisher/requirements.txt +++ b/cloud-media-livestream/keypublisher/requirements.txt @@ -1,11 +1,11 @@ -Flask==2.2.5 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-secret-manager==2.21.1 -lxml==5.2.1 +lxml==6.1.1 pycryptodome==3.21.0 -pyOpenSSL==25.0.0 -requests==2.32.4 +pyOpenSSL==26.3.0 +requests==2.34.2; python_version >= '3.10' signxml==4.0.4 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-mock==3.14.0 -Werkzeug==3.0.6 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/cloud-sql/mysql/client-side-encryption/noxfile_config.py b/cloud-sql/mysql/client-side-encryption/noxfile_config.py index 67016bd9634..322efba33e8 100644 --- a/cloud-sql/mysql/client-side-encryption/noxfile_config.py +++ b/cloud-sql/mysql/client-side-encryption/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.6", "3.10", "3.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/cloud-sql/mysql/client-side-encryption/requirements-test.txt b/cloud-sql/mysql/client-side-encryption/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud-sql/mysql/client-side-encryption/requirements-test.txt +++ b/cloud-sql/mysql/client-side-encryption/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud-sql/mysql/client-side-encryption/requirements.txt b/cloud-sql/mysql/client-side-encryption/requirements.txt index 32f632b2ca7..52b5fac5d02 100644 --- a/cloud-sql/mysql/client-side-encryption/requirements.txt +++ b/cloud-sql/mysql/client-side-encryption/requirements.txt @@ -1,3 +1,3 @@ SQLAlchemy==2.0.40 -PyMySQL==1.1.1 +PyMySQL==1.1.2 tink==1.9.0 diff --git a/cloud-sql/mysql/sqlalchemy/Dockerfile b/cloud-sql/mysql/sqlalchemy/Dockerfile index 72a0ef555e1..d0ad90b7fbb 100644 --- a/cloud-sql/mysql/sqlalchemy/Dockerfile +++ b/cloud-sql/mysql/sqlalchemy/Dockerfile @@ -14,7 +14,7 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.13 +FROM python:3.14 RUN apt-get update diff --git a/cloud-sql/mysql/sqlalchemy/requirements-test.txt b/cloud-sql/mysql/sqlalchemy/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud-sql/mysql/sqlalchemy/requirements-test.txt +++ b/cloud-sql/mysql/sqlalchemy/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud-sql/mysql/sqlalchemy/requirements.txt b/cloud-sql/mysql/sqlalchemy/requirements.txt index 9ad6f872bc5..19acd5abd52 100644 --- a/cloud-sql/mysql/sqlalchemy/requirements.txt +++ b/cloud-sql/mysql/sqlalchemy/requirements.txt @@ -1,7 +1,7 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' SQLAlchemy==2.0.40 -PyMySQL==1.1.1 +PyMySQL==1.1.2 gunicorn==23.0.0 -cloud-sql-python-connector==1.18.2 +cloud-sql-python-connector==1.20.1 functions-framework==3.9.2 -Werkzeug==2.3.8 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/cloud-sql/postgres/client-side-encryption/noxfile_config.py b/cloud-sql/postgres/client-side-encryption/noxfile_config.py index 67016bd9634..322efba33e8 100644 --- a/cloud-sql/postgres/client-side-encryption/noxfile_config.py +++ b/cloud-sql/postgres/client-side-encryption/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.6", "3.10", "3.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/cloud-sql/postgres/client-side-encryption/requirements-test.txt b/cloud-sql/postgres/client-side-encryption/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud-sql/postgres/client-side-encryption/requirements-test.txt +++ b/cloud-sql/postgres/client-side-encryption/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud-sql/postgres/client-side-encryption/requirements.txt b/cloud-sql/postgres/client-side-encryption/requirements.txt index 1749cee78fb..1ec3e93d497 100644 --- a/cloud-sql/postgres/client-side-encryption/requirements.txt +++ b/cloud-sql/postgres/client-side-encryption/requirements.txt @@ -1,3 +1,3 @@ SQLAlchemy==2.0.40 -pg8000==1.31.2 +pg8000==1.31.5 tink==1.9.0 diff --git a/cloud-sql/postgres/sqlalchemy/Dockerfile b/cloud-sql/postgres/sqlalchemy/Dockerfile index 72a0ef555e1..d0ad90b7fbb 100644 --- a/cloud-sql/postgres/sqlalchemy/Dockerfile +++ b/cloud-sql/postgres/sqlalchemy/Dockerfile @@ -14,7 +14,7 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.13 +FROM python:3.14 RUN apt-get update diff --git a/cloud-sql/postgres/sqlalchemy/requirements-test.txt b/cloud-sql/postgres/sqlalchemy/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud-sql/postgres/sqlalchemy/requirements-test.txt +++ b/cloud-sql/postgres/sqlalchemy/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud-sql/postgres/sqlalchemy/requirements.txt b/cloud-sql/postgres/sqlalchemy/requirements.txt index 77c7218ef61..900d922d5f4 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.2 +Flask==3.1.3; python_version >= '3.9' +pg8000==1.31.5 SQLAlchemy==2.0.40 -cloud-sql-python-connector==1.18.2 +cloud-sql-python-connector==1.20.1 gunicorn==23.0.0 functions-framework==3.9.2 -Werkzeug==2.3.8 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/cloud-sql/sql-server/client-side-encryption/noxfile_config.py b/cloud-sql/sql-server/client-side-encryption/noxfile_config.py index 692cb4ecd26..c74cec2355d 100644 --- a/cloud-sql/sql-server/client-side-encryption/noxfile_config.py +++ b/cloud-sql/sql-server/client-side-encryption/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.6", "3.10", "3.11"], + "ignored_versions": ["3.8", "3.9", "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/cloud-sql/sql-server/client-side-encryption/requirements-test.txt b/cloud-sql/sql-server/client-side-encryption/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud-sql/sql-server/client-side-encryption/requirements-test.txt +++ b/cloud-sql/sql-server/client-side-encryption/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud-sql/sql-server/sqlalchemy/Dockerfile b/cloud-sql/sql-server/sqlalchemy/Dockerfile index 75f4e22a969..1464006d45b 100644 --- a/cloud-sql/sql-server/sqlalchemy/Dockerfile +++ b/cloud-sql/sql-server/sqlalchemy/Dockerfile @@ -14,7 +14,7 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.13 +FROM python:3.14 RUN apt-get update diff --git a/cloud-sql/sql-server/sqlalchemy/requirements-test.txt b/cloud-sql/sql-server/sqlalchemy/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud-sql/sql-server/sqlalchemy/requirements-test.txt +++ b/cloud-sql/sql-server/sqlalchemy/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud-sql/sql-server/sqlalchemy/requirements.txt b/cloud-sql/sql-server/sqlalchemy/requirements.txt index 112da74c209..17a6f71b96c 100644 --- a/cloud-sql/sql-server/sqlalchemy/requirements.txt +++ b/cloud-sql/sql-server/sqlalchemy/requirements.txt @@ -1,9 +1,9 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 python-tds==1.16.0 -pyopenssl==25.0.0 +pyopenssl==26.0.0 SQLAlchemy==2.0.40 -cloud-sql-python-connector==1.18.2 +cloud-sql-python-connector==1.20.1 sqlalchemy-pytds==1.0.2 functions-framework==3.9.2 -Werkzeug==2.3.8 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/cloud_scheduler/snippets/noxfile_config.py b/cloud_scheduler/snippets/noxfile_config.py index 9a4b880f934..0973c8621c7 100644 --- a/cloud_scheduler/snippets/noxfile_config.py +++ b/cloud_scheduler/snippets/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.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/cloud_scheduler/snippets/requirements-test.txt b/cloud_scheduler/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud_scheduler/snippets/requirements-test.txt +++ b/cloud_scheduler/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloud_scheduler/snippets/requirements.txt b/cloud_scheduler/snippets/requirements.txt index e95a2ef8c50..79010255176 100644 --- a/cloud_scheduler/snippets/requirements.txt +++ b/cloud_scheduler/snippets/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 google-cloud-scheduler==2.14.1 -Werkzeug==3.0.6 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/cloud_tasks/http_queues/requirements-test.txt b/cloud_tasks/http_queues/requirements-test.txt index 5e1e631ee52..62b9771c178 100644 --- a/cloud_tasks/http_queues/requirements-test.txt +++ b/cloud_tasks/http_queues/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-auth==2.38.0 google-api-core==2.17.1 diff --git a/cloud_tasks/http_queues/requirements.txt b/cloud_tasks/http_queues/requirements.txt index de6af1800a9..f0c0b8a5f19 100644 --- a/cloud_tasks/http_queues/requirements.txt +++ b/cloud_tasks/http_queues/requirements.txt @@ -1,2 +1,2 @@ google-cloud-tasks==2.18.0 -requests==2.32.4 \ No newline at end of file +requests==2.34.2; python_version >= '3.10' diff --git a/cloud_tasks/snippets/noxfile_config.py b/cloud_tasks/snippets/noxfile_config.py index 359b876b767..1b9d9e4a580 100644 --- a/cloud_tasks/snippets/noxfile_config.py +++ b/cloud_tasks/snippets/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.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/cloud_tasks/snippets/requirements-test.txt b/cloud_tasks/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/cloud_tasks/snippets/requirements-test.txt +++ b/cloud_tasks/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/cloudbuild/snippets/noxfile_config.py b/cloudbuild/snippets/noxfile_config.py index 35d32a1f9e4..322efba33e8 100644 --- a/cloudbuild/snippets/noxfile_config.py +++ b/cloudbuild/snippets/noxfile_config.py @@ -22,8 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # NOTE: We currently only run the test in Python 3.9. - "ignored_versions": ["2.7", "3.7", "3.8", "3.10", "3.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/cloudbuild/snippets/requirements-test.txt b/cloudbuild/snippets/requirements-test.txt index 060ed652e0b..c9e154ba440 100644 --- a/cloudbuild/snippets/requirements-test.txt +++ b/cloudbuild/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" 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/2022_airflow_summit/requirements.txt b/composer/2022_airflow_summit/requirements.txt index 50a4b3df15b..5b304e055a5 100644 --- a/composer/2022_airflow_summit/requirements.txt +++ b/composer/2022_airflow_summit/requirements.txt @@ -1,4 +1,4 @@ # be sure to update the constraints file to match # see https://airflow.apache.org/docs/apache-airflow/stable/installation/installing-from-pypi.html#constraints-files -apache-airflow==2.6.3 -apache-airflow-providers-google==10.2.0 +apache-airflow==3.2.1; python_version >= "3.10" +apache-airflow-providers-google==21.3.0; python_version >= "3.10" diff --git a/composer/airflow_1_samples/gke_operator.py b/composer/airflow_1_samples/gke_operator.py index b3638655b20..410a5a2aa7d 100644 --- a/composer/airflow_1_samples/gke_operator.py +++ b/composer/airflow_1_samples/gke_operator.py @@ -92,12 +92,12 @@ # 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] # [START composer_gkeoperator_templateconfig_airflow_1] - kubenetes_template_ex = GKEStartPodOperator( + kubernetes_template_ex = GKEStartPodOperator( task_id="ex-kube-templates", name="ex-kube-templates", project_id=PROJECT_ID, @@ -243,6 +243,6 @@ create_cluster >> create_node_pools >> kubernetes_min_pod >> delete_cluster create_cluster >> create_node_pools >> kubernetes_full_pod >> delete_cluster create_cluster >> create_node_pools >> kubernetes_affinity_ex >> delete_cluster - create_cluster >> create_node_pools >> kubenetes_template_ex >> delete_cluster + create_cluster >> create_node_pools >> kubernetes_template_ex >> delete_cluster # [END composer_gkeoperator_airflow_1] diff --git a/composer/airflow_1_samples/kubernetes_pod_operator.py b/composer/airflow_1_samples/kubernetes_pod_operator.py index 11abdb6b1ec..2062d2cc636 100644 --- a/composer/airflow_1_samples/kubernetes_pod_operator.py +++ b/composer/airflow_1_samples/kubernetes_pod_operator.py @@ -93,11 +93,11 @@ # 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] - kubenetes_template_ex = kubernetes_pod_operator.KubernetesPodOperator( + kubernetes_template_ex = kubernetes_pod_operator.KubernetesPodOperator( task_id="ex-kube-templates", name="ex-kube-templates", namespace="default", 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/airflow_1_samples/requirements-test.txt b/composer/airflow_1_samples/requirements-test.txt index c09fc77b516..81d6f3c664b 100644 --- a/composer/airflow_1_samples/requirements-test.txt +++ b/composer/airflow_1_samples/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.1.1 cloud-composer-dag-test-utils==0.0.1 diff --git a/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements-test.txt b/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements-test.txt index 02122fa48a5..4defcdeb160 100644 --- a/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements-test.txt +++ b/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 -cloud-composer-dag-test-utils==1.0.0 \ No newline at end of file +pytest==9.1.1 +cloud-composer-dag-test-utils==1.0.0 diff --git a/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements.txt b/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements.txt index fc3c4940fa5..bf1282437f0 100644 --- a/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements.txt +++ b/composer/blog/gcp-tech-blog/data-orchestration-with-composer/requirements.txt @@ -1,6 +1,6 @@ # be sure to update the constraints file to match # see https://airflow.apache.org/docs/apache-airflow/stable/installation/installing-from-pypi.html#constraints-files -apache-airflow[google]==2.6.3 +apache-airflow[google]==3.2.1; python_version >= "3.10" apache-airflow-providers-apache-beam==5.1.1 apache-airflow-providers-slack==7.3.2 apache-airflow-providers-http==4.4.2 diff --git a/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/Dockerfile b/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/Dockerfile index e78e11b3462..a18789f2f09 100644 --- a/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/Dockerfile +++ b/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM python:3.11 +FROM python:3.14 # Allow statements and log messages to immediately appear in the Cloud Run logs ENV PYTHONUNBUFFERED True diff --git a/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/noxfile_config.py b/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/noxfile_config.py index 5751b6d12eb..9577c4f3b42 100644 --- a/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/noxfile_config.py +++ b/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/noxfile_config.py @@ -32,6 +32,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. # Skipping for Python 3.9 due to numpy compilation failure. + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) "ignored_versions": ["2.7", "3.9", "3.10", "3.11"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them diff --git a/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/requirements-test.txt b/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/requirements-test.txt index 02122fa48a5..4defcdeb160 100644 --- a/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/requirements-test.txt +++ b/composer/blog/gcp-tech-blog/unit-test-dags-cloud-build/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 -cloud-composer-dag-test-utils==1.0.0 \ No newline at end of file +pytest==9.1.1 +cloud-composer-dag-test-utils==1.0.0 diff --git a/composer/cicd_sample/requirements-test.txt b/composer/cicd_sample/requirements-test.txt index 02122fa48a5..4defcdeb160 100644 --- a/composer/cicd_sample/requirements-test.txt +++ b/composer/cicd_sample/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 -cloud-composer-dag-test-utils==1.0.0 \ No newline at end of file +pytest==9.1.1 +cloud-composer-dag-test-utils==1.0.0 diff --git a/composer/cicd_sample/requirements.txt b/composer/cicd_sample/requirements.txt index 1eb3eeda323..5217f867c57 100644 --- a/composer/cicd_sample/requirements.txt +++ b/composer/cicd_sample/requirements.txt @@ -1,3 +1,3 @@ # be sure to update the constraints file to match # see https://airflow.apache.org/docs/apache-airflow/stable/installation/installing-from-pypi.html#constraints-files -apache-airflow==2.6.3 +apache-airflow==3.2.1; python_version >= "3.10" diff --git a/composer/cicd_sample/utils/add_dags_to_composer.py b/composer/cicd_sample/utils/add_dags_to_composer.py index 8e5698f0ba8..7df54ff52fb 100644 --- a/composer/cicd_sample/utils/add_dags_to_composer.py +++ b/composer/cicd_sample/utils/add_dags_to_composer.py @@ -57,7 +57,7 @@ def upload_dags_to_composer( if len(dags) > 0: # Note - the GCS client library does not currently support batch requests on uploads # if you have a large number of files, consider using - # the Python subprocess module to run gsutil -m cp -r on your dags + # the Python subprocess module to run gcloud storage cp --recursive on your dags # See https://cloud.google.com/storage/docs/gsutil/commands/cp for more info storage_client = storage.Client() bucket = storage_client.bucket(bucket_name) diff --git a/composer/cicd_sample/utils/noxfile_config.py b/composer/cicd_sample/utils/noxfile_config.py index 1583c4e637d..59eb70b5b1c 100644 --- a/composer/cicd_sample/utils/noxfile_config.py +++ b/composer/cicd_sample/utils/noxfile_config.py @@ -30,15 +30,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": [ - "2.7", - "3.6", - "3.7", - "3.9", - "3.10", - "3.11", - "3.12", - ], # Composer w/ Airflow 2 only supports Python 3.8 + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Composer w/ Airflow 2 only supports Python 3.8 # Old samples are opted out of enforcing Python type hints # All new samples should feature them "enforce_type_hints": True, diff --git a/composer/cicd_sample/utils/requirements-test.txt b/composer/cicd_sample/utils/requirements-test.txt index 92e25bbd179..a4a630eb4a0 100644 --- a/composer/cicd_sample/utils/requirements-test.txt +++ b/composer/cicd_sample/utils/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==8.2.0 -requests==2.31.0 +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' google-api-core==2.17.1 google-resumable-media==2.7.2 diff --git a/composer/functions/requirements-test.txt b/composer/functions/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/composer/functions/requirements-test.txt +++ b/composer/functions/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/composer/rest/composer2/requirements-test.txt b/composer/rest/composer2/requirements-test.txt index 6420b5190a5..6a4b7adfd6e 100644 --- a/composer/rest/composer2/requirements-test.txt +++ b/composer/rest/composer2/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 -requests==2.32.2 +pytest==9.0.3; python_version >= "3.10" +requests==2.34.2; python_version >= '3.10' diff --git a/composer/rest/composer2/requirements.txt b/composer/rest/composer2/requirements.txt index 9e210499090..c8864de5b34 100644 --- a/composer/rest/composer2/requirements.txt +++ b/composer/rest/composer2/requirements.txt @@ -1,2 +1,2 @@ google-auth==2.38.0 -requests==2.32.2 +requests==2.34.2; python_version >= '3.10' diff --git a/composer/rest/requirements-test.txt b/composer/rest/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/composer/rest/requirements-test.txt +++ b/composer/rest/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/composer/rest/requirements.txt b/composer/rest/requirements.txt index d008de40fc4..6833a6e1bbc 100644 --- a/composer/rest/requirements.txt +++ b/composer/rest/requirements.txt @@ -1,3 +1,3 @@ google-auth==2.38.0 -requests==2.32.4 +requests==2.34.2; python_version >= '3.10' six==1.16.0 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..acf60c05e5a 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,12 +86,12 @@ # 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] # [START composer_gkeoperator_templateconfig] - kubenetes_template_ex = GKEStartPodOperator( + kubernetes_template_ex = GKEStartPodOperator( task_id="ex-kube-templates", name="ex-kube-templates", project_id=PROJECT_ID, @@ -238,6 +238,6 @@ create_cluster >> kubernetes_min_pod >> delete_cluster create_cluster >> kubernetes_full_pod >> delete_cluster create_cluster >> kubernetes_affinity_ex >> delete_cluster - create_cluster >> kubenetes_template_ex >> delete_cluster + create_cluster >> kubernetes_template_ex >> delete_cluster # [END composer_gkeoperator] diff --git a/composer/workflows/kubernetes_pod_operator.py b/composer/workflows/kubernetes_pod_operator.py index f679ead81d7..835bd108fd4 100644 --- a/composer/workflows/kubernetes_pod_operator.py +++ b/composer/workflows/kubernetes_pod_operator.py @@ -96,11 +96,11 @@ # 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] - kubenetes_template_ex = KubernetesPodOperator( + kubernetes_template_ex = KubernetesPodOperator( task_id="ex-kube-templates", name="ex-kube-templates", namespace="default", 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/composer/workflows/requirements-test.txt b/composer/workflows/requirements-test.txt index 6045349ed57..4defcdeb160 100644 --- a/composer/workflows/requirements-test.txt +++ b/composer/workflows/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.3.2 +pytest==9.1.1 cloud-composer-dag-test-utils==1.0.0 diff --git a/compute/api/README.md b/compute/api/README.md deleted file mode 100644 index 757a4a2a79a..00000000000 --- a/compute/api/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Compute Engine API 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=compute/api/README.md - - -These samples are used on the following documentation pages: - -> -* https://cloud.google.com/compute/docs/tutorials/python-guide -* https://cloud.google.com/compute/docs/instances/create-start-instance -* https://cloud.google.com/compute/docs/api/how-tos/api-requests-responses - - - - - - - diff --git a/compute/api/create_instance.py b/compute/api/create_instance.py deleted file mode 100644 index 9542ef7fd3a..00000000000 --- a/compute/api/create_instance.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2015 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. - -"""Example of using the Compute Engine API to create and delete instances. - -Creates a new compute engine instance and uses it to apply a caption to -an image. - - https://cloud.google.com/compute/docs/tutorials/python-guide - -For more information, see the README.md under /compute. -""" - -import argparse -import os -import time - -import googleapiclient.discovery - - -def list_instances( - compute: object, - project: str, - zone: str, -) -> list: - """Lists all instances in the specified zone. - - Args: - compute: an initialized compute service object. - project: the Google Cloud project ID. - zone: the name of the zone in which the instances should be listed. - - Returns: - A list of instances. - """ - result = compute.instances().list(project=project, zone=zone).execute() - return result["items"] if "items" in result else None - - -# [START compute_create_instance] -def create_instance( - compute: object, - project: str, - zone: str, - name: str, - bucket: str, -) -> str: - """Creates an instance in the specified zone. - - Args: - compute: an initialized compute service object. - project: the Google Cloud project ID. - zone: the name of the zone in which the instances should be created. - name: the name of the instance. - bucket: the name of the bucket in which the image should be written. - - Returns: - The instance object. - """ - # Get the latest Debian Jessie image. - image_response = ( - compute.images() - .getFromFamily(project="debian-cloud", family="debian-11") - .execute() - ) - source_disk_image = image_response["selfLink"] - - # Configure the machine - machine_type = "zones/%s/machineTypes/n1-standard-1" % zone - startup_script = open( - os.path.join(os.path.dirname(__file__), "startup-script.sh") - ).read() - image_url = "http://storage.googleapis.com/gce-demo-input/photo.jpg" - image_caption = "Ready for dessert?" - - config = { - "name": name, - "machineType": machine_type, - # Specify the boot disk and the image to use as a source. - "disks": [ - { - "boot": True, - "autoDelete": True, - "initializeParams": { - "sourceImage": source_disk_image, - }, - } - ], - # Specify a network interface with NAT to access the public - # internet. - "networkInterfaces": [ - { - "network": "global/networks/default", - "accessConfigs": [{"type": "ONE_TO_ONE_NAT", "name": "External NAT"}], - } - ], - # Allow the instance to access cloud storage and logging. - "serviceAccounts": [ - { - "email": "default", - "scopes": [ - "https://www.googleapis.com/auth/devstorage.read_write", - "https://www.googleapis.com/auth/logging.write", - ], - } - ], - # Metadata is readable from the instance and allows you to - # pass configuration from deployment scripts to instances. - "metadata": { - "items": [ - { - # Startup script is automatically executed by the - # instance upon startup. - "key": "startup-script", - "value": startup_script, - }, - {"key": "url", "value": image_url}, - {"key": "text", "value": image_caption}, - {"key": "bucket", "value": bucket}, - ] - }, - } - - return compute.instances().insert(project=project, zone=zone, body=config).execute() - - -# [END compute_create_instance] - - -def delete_instance( - compute: object, - project: str, - zone: str, - name: str, -) -> str: - """Deletes an instance. - - Args: - compute: An initialized compute service object. - project: The Google Cloud project ID. - zone: The name of the zone in which the instances should be deleted. - name: The name of the instance. - - Returns: - Execute to delete the instance object. - """ - return ( - compute.instances().delete(project=project, zone=zone, instance=name).execute() - ) - - -# [START compute_wait_for_operation] -def wait_for_operation( - compute: object, - project: str, - zone: str, - operation: str, -) -> dict: - """Waits for the given operation to complete. - - Args: - compute: an initialized compute service object. - project: the Google Cloud project ID. - zone: the name of the zone in which the operation should be executed. - operation: the operation ID. - - Returns: - The result of the operation. - """ - print("Waiting for operation to finish...") - while True: - result = ( - compute.zoneOperations() - .get(project=project, zone=zone, operation=operation) - .execute() - ) - - if result["status"] == "DONE": - print("done.") - if "error" in result: - raise Exception(result["error"]) - return result - - time.sleep(1) - - -# [END compute_wait_for_operation] - - -def main( - project: str, - bucket: str, - zone: str, - instance_name: str, - wait=True, -) -> None: - """Runs the demo. - - Args: - project: the Google Cloud project ID. - bucket: the name of the bucket in which the image should be written. - instance_name: the name of the instance. - wait: whether to wait for the operation to complete. - - Returns: - None. - """ - compute = googleapiclient.discovery.build("compute", "v1") - - print("Creating instance.") - - operation = create_instance(compute, project, zone, instance_name, bucket) - wait_for_operation(compute, project, zone, operation["name"]) - - instances = list_instances(compute, project, zone) - - print(f"Instances in project {project} and zone {zone}:") - for instance in instances: - print(f' - {instance["name"]}') - - print( - f""" -Instance created. -It will take a minute or two for the instance to complete work. -Check this URL: http://storage.googleapis.com/{bucket}/output.png -Once the image is uploaded press enter to delete the instance. -""" - ) - - if wait: - input() - - print("Deleting instance.") - - operation = delete_instance(compute, project, zone, instance_name) - wait_for_operation(compute, project, zone, operation["name"]) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter - ) - parser.add_argument("project_id", help="Your Google Cloud project ID.") - parser.add_argument("bucket_name", help="Your Google Cloud Storage bucket name.") - parser.add_argument( - "--zone", default="us-central1-f", help="Compute Engine zone to deploy to." - ) - parser.add_argument("--name", default="demo-instance", help="New instance name.") - - args = parser.parse_args() - - main(args.project_id, args.bucket_name, args.zone, args.name) diff --git a/compute/api/create_instance_test.py b/compute/api/create_instance_test.py deleted file mode 100644 index 9c24f860656..00000000000 --- a/compute/api/create_instance_test.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2015 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 uuid - -import pytest - -from create_instance import main - -PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] -BUCKET = os.environ["CLOUD_STORAGE_BUCKET"] - - -@pytest.mark.flaky(max_runs=3, min_passes=1) -def test_main(capsys): - instance_name = f"test-instance-{uuid.uuid4()}" - main(PROJECT, BUCKET, "europe-west1-b", instance_name, wait=False) - - out, _ = capsys.readouterr() - - assert "Instances in project" in out - assert "zone europe-west1-b" in out - assert instance_name in out - assert "Deleting instance" in out diff --git a/compute/api/requirements-test.txt b/compute/api/requirements-test.txt deleted file mode 100644 index 185d62c4204..00000000000 --- a/compute/api/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==8.2.0 -flaky==3.8.1 diff --git a/compute/api/requirements.txt b/compute/api/requirements.txt deleted file mode 100644 index 7f4398de541..00000000000 --- a/compute/api/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -google-api-python-client==2.131.0 -google-auth==2.38.0 -google-auth-httplib2==0.2.0 diff --git a/compute/api/startup-script.sh b/compute/api/startup-script.sh deleted file mode 100644 index 806779accf4..00000000000 --- a/compute/api/startup-script.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Copyright 2015 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. - -apt-get update -apt-get -y install imagemagick - -# Use the metadata server to get the configuration specified during -# instance creation. Read more about metadata here: -# https://cloud.google.com/compute/docs/metadata#querying -IMAGE_URL=$(curl http://metadata/computeMetadata/v1/instance/attributes/url -H "Metadata-Flavor: Google") -TEXT=$(curl http://metadata/computeMetadata/v1/instance/attributes/text -H "Metadata-Flavor: Google") -CS_BUCKET=$(curl http://metadata/computeMetadata/v1/instance/attributes/bucket -H "Metadata-Flavor: Google") - -mkdir image-output -cd image-output -wget $IMAGE_URL -convert * -pointsize 30 -fill white -stroke black -gravity center -annotate +10+40 "$TEXT" output.png - -# Create a Google Cloud Storage bucket. -gsutil mb gs://$CS_BUCKET - -# Store the image in the Google Cloud Storage bucket and allow all users -# to read it. -gsutil cp -a public-read output.png gs://$CS_BUCKET/output.png diff --git a/compute/auth/noxfile_config.py b/compute/auth/noxfile_config.py index 457e86f5413..0973c8621c7 100644 --- a/compute/auth/noxfile_config.py +++ b/compute/auth/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/compute/auth/requirements-test.txt b/compute/auth/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/compute/auth/requirements-test.txt +++ b/compute/auth/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/compute/auth/requirements.txt b/compute/auth/requirements.txt index 47ad86a4a81..0f644bbee2f 100644 --- a/compute/auth/requirements.txt +++ b/compute/auth/requirements.txt @@ -1,4 +1,4 @@ -requests==2.32.4 +requests==2.34.2; python_version >= '3.10' google-auth==2.38.0 google-auth-httplib2==0.2.0 google-cloud-storage==2.9.0 diff --git a/compute/client_library/ingredients/operations/wait_for_operation.py b/compute/client_library/ingredients/operations/wait_for_operation.py index fdaf53d5594..055e62bef1d 100644 --- a/compute/client_library/ingredients/operations/wait_for_operation.py +++ b/compute/client_library/ingredients/operations/wait_for_operation.py @@ -17,6 +17,7 @@ # Disabling flake8 for the ingredients file, as it would fail F821 - undefined name check. # flake8: noqa from google.cloud import compute_v1 +import time # @@ -46,7 +47,18 @@ def wait_for_operation( kwargs["region"] = operation.region.rsplit("/", maxsplit=1)[1] else: client = compute_v1.GlobalOperationsClient() - return client.wait(**kwargs) + + while True: + result = client.get(**kwargs) + + if result.status == compute_v1.Operation.Status.DONE: + print("Operation finished.") + if result.error: + print(f"Error during operation: {result.error}") + return result + + print("Waiting for operation to complete...") + time.sleep(2) # diff --git a/compute/client_library/requirements-test.txt b/compute/client_library/requirements-test.txt index 32f96d024ee..567e8b4fac2 100644 --- a/compute/client_library/requirements-test.txt +++ b/compute/client_library/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==8.3.2 +pytest==9.0.3; python_version >= "3.10" pytest-xdist==3.6.1 flaky==3.8.1 google-cloud-storage==2.18.0 diff --git a/compute/client_library/requirements.txt b/compute/client_library/requirements.txt index f9faea10a9d..656732bef9a 100644 --- a/compute/client_library/requirements.txt +++ b/compute/client_library/requirements.txt @@ -1,5 +1,4 @@ isort==6.0.0; python_version > "3.9" isort==5.13.2; python_version <= "3.8" -black==24.8.0; python_version < "3.9" -black==24.10.0; python_version >= "3.9" -google-cloud-compute==1.19.1 \ No newline at end of file +black==26.3.1 +google-cloud-compute==1.19.1 diff --git a/compute/client_library/snippets/operations/operation_check.py b/compute/client_library/snippets/operations/operation_check.py index a022cd459fa..8216cef172a 100644 --- a/compute/client_library/snippets/operations/operation_check.py +++ b/compute/client_library/snippets/operations/operation_check.py @@ -20,6 +20,8 @@ # [START compute_instances_operation_check] +import time + from google.cloud import compute_v1 @@ -49,7 +51,18 @@ def wait_for_operation( kwargs["region"] = operation.region.rsplit("/", maxsplit=1)[1] else: client = compute_v1.GlobalOperationsClient() - return client.wait(**kwargs) + + while True: + result = client.get(**kwargs) + + if result.status == compute_v1.Operation.Status.DONE: + print("Operation finished.") + if result.error: + print(f"Error during operation: {result.error}") + return result + + print("Waiting for operation to complete...") + time.sleep(2) # [END compute_instances_operation_check] diff --git a/compute/client_library/snippets/usage_report/usage_reports.py b/compute/client_library/snippets/usage_report/usage_reports.py index 257d8a092d2..bccf15fb73e 100644 --- a/compute/client_library/snippets/usage_report/usage_reports.py +++ b/compute/client_library/snippets/usage_report/usage_reports.py @@ -34,7 +34,6 @@ from google.api_core.extended_operation import ExtendedOperation from google.cloud import compute_v1 - # [END compute_usage_report_disable] # [END compute_usage_report_get] # [END compute_usage_report_set] diff --git a/compute/encryption/noxfile_config.py b/compute/encryption/noxfile_config.py index 457e86f5413..0973c8621c7 100644 --- a/compute/encryption/noxfile_config.py +++ b/compute/encryption/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/compute/encryption/requirements-test.txt b/compute/encryption/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/compute/encryption/requirements-test.txt +++ b/compute/encryption/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/compute/encryption/requirements.txt b/compute/encryption/requirements.txt index ca64bbbc0f4..6b676b1c188 100644 --- a/compute/encryption/requirements.txt +++ b/compute/encryption/requirements.txt @@ -1,5 +1,5 @@ -cryptography==45.0.1 -requests==2.32.4 +cryptography==48.0.1 +requests==2.34.2 google-api-python-client==2.131.0 google-auth==2.38.0 google-auth-httplib2==0.2.0 diff --git a/compute/load_balancing/requirements-test.txt b/compute/load_balancing/requirements-test.txt index 060ed652e0b..c9e154ba440 100644 --- a/compute/load_balancing/requirements-test.txt +++ b/compute/load_balancing/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/compute/managed-instances/demo/requirements-test.txt b/compute/managed-instances/demo/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/compute/managed-instances/demo/requirements-test.txt +++ b/compute/managed-instances/demo/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/compute/managed-instances/demo/requirements.txt b/compute/managed-instances/demo/requirements.txt index 73c648b4ffc..80e0432d9a7 100644 --- a/compute/managed-instances/demo/requirements.txt +++ b/compute/managed-instances/demo/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 -requests==2.31.0 -Werkzeug==3.0.3 +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' diff --git a/compute/metadata/requirements-test.txt b/compute/metadata/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/compute/metadata/requirements-test.txt +++ b/compute/metadata/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/compute/metadata/requirements.txt b/compute/metadata/requirements.txt index d03212dcf9c..e41a7179eed 100644 --- a/compute/metadata/requirements.txt +++ b/compute/metadata/requirements.txt @@ -1,2 +1,2 @@ -requests==2.32.4 -google-auth==2.38.0 \ No newline at end of file +requests==2.34.2; python_version >= '3.10' +google-auth==2.38.0 diff --git a/compute/oslogin/requirements-test.txt b/compute/oslogin/requirements-test.txt index a8518ad953b..e30f061590f 100644 --- a/compute/oslogin/requirements-test.txt +++ b/compute/oslogin/requirements-test.txt @@ -1,5 +1,5 @@ backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-iam==2.17.0 google-api-python-client==2.131.0 diff --git a/compute/oslogin/requirements.txt b/compute/oslogin/requirements.txt index f77e111b4e9..3611f7bdf44 100644 --- a/compute/oslogin/requirements.txt +++ b/compute/oslogin/requirements.txt @@ -3,4 +3,4 @@ google-auth==2.38.0 google-auth-httplib2==0.2.0 google-cloud-compute==1.11.0 google-cloud-os-login==2.15.1 -requests==2.32.4 \ No newline at end of file +requests==2.34.2; python_version >= '3.10' diff --git a/connectgateway/noxfile_config.py b/connectgateway/noxfile_config.py index ea71c27ca40..192bea7d9f8 100644 --- a/connectgateway/noxfile_config.py +++ b/connectgateway/noxfile_config.py @@ -14,7 +14,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], "enforce_type_hints": True, "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", "pip_version_override": None, diff --git a/connectgateway/requirements-test.txt b/connectgateway/requirements-test.txt index 8c22c500206..b945dc72083 100644 --- a/connectgateway/requirements-test.txt +++ b/connectgateway/requirements-test.txt @@ -1,2 +1,2 @@ google-cloud-container==2.56.1 -pytest==8.3.5 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/connectgateway/requirements.txt b/connectgateway/requirements.txt index 7141c827fa1..531ee9e7eb4 100644 --- a/connectgateway/requirements.txt +++ b/connectgateway/requirements.txt @@ -1,4 +1,4 @@ google-cloud-gke-connect-gateway==0.10.4 google-auth==2.38.0 -kubernetes==33.1.0 +kubernetes==34.1.0 google-api-core==2.24.2 diff --git a/contact-center-insights/snippets/noxfile_config.py b/contact-center-insights/snippets/noxfile_config.py index 2be8b96be22..309b7dcec18 100644 --- a/contact-center-insights/snippets/noxfile_config.py +++ b/contact-center-insights/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/contact-center-insights/snippets/requirements-test.txt b/contact-center-insights/snippets/requirements-test.txt index 63f2d349e99..2d33e042cc1 100644 --- a/contact-center-insights/snippets/requirements-test.txt +++ b/contact-center-insights/snippets/requirements-test.txt @@ -1,3 +1,3 @@ google-auth==2.38.0 google-cloud-pubsub==2.28.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/container/snippets/noxfile_config.py b/container/snippets/noxfile_config.py index 9f90577041b..1b9d9e4a580 100644 --- a/container/snippets/noxfile_config.py +++ b/container/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/container/snippets/requirements.txt b/container/snippets/requirements.txt index 8f29e2f0eb9..e0a9e8cbe81 100644 --- a/container/snippets/requirements.txt +++ b/container/snippets/requirements.txt @@ -1,3 +1,3 @@ google-cloud-container==2.54.0 backoff==2.2.1 -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/containeranalysis/snippets/requirements-test.txt b/containeranalysis/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/containeranalysis/snippets/requirements-test.txt +++ b/containeranalysis/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/containeranalysis/snippets/requirements.txt b/containeranalysis/snippets/requirements.txt index 25ce20b0657..8118ef068de 100644 --- a/containeranalysis/snippets/requirements.txt +++ b/containeranalysis/snippets/requirements.txt @@ -1,6 +1,6 @@ google-cloud-pubsub==2.28.0 google-cloud-containeranalysis==2.16.0 grafeas==1.12.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 mock==5.1.0 diff --git a/contentwarehouse/snippets/noxfile_config.py b/contentwarehouse/snippets/noxfile_config.py index a85697d4eb5..b7d8115d773 100644 --- a/contentwarehouse/snippets/noxfile_config.py +++ b/contentwarehouse/snippets/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.6", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/contentwarehouse/snippets/requirements-test.txt b/contentwarehouse/snippets/requirements-test.txt index d752d70577e..8f9d5a165a3 100644 --- a/contentwarehouse/snippets/requirements-test.txt +++ b/contentwarehouse/snippets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-resource-manager==1.10.1 diff --git a/datacatalog/quickstart/requirements-test.txt b/datacatalog/quickstart/requirements-test.txt index 11dc8bbd341..b1d77d220e8 100644 --- a/datacatalog/quickstart/requirements-test.txt +++ b/datacatalog/quickstart/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-bigquery==3.27.0 \ No newline at end of file diff --git a/datacatalog/snippets/requirements-test.txt b/datacatalog/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/datacatalog/snippets/requirements-test.txt +++ b/datacatalog/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/datacatalog/v1beta1/requirements-test.txt b/datacatalog/v1beta1/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/datacatalog/v1beta1/requirements-test.txt +++ b/datacatalog/v1beta1/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/dataflow/conftest.py b/dataflow/conftest.py index a1f81eac6f6..2bb6c6b3ecf 100644 --- a/dataflow/conftest.py +++ b/dataflow/conftest.py @@ -85,7 +85,7 @@ def bucket_name(test_name: str, location: str, unique_id: str) -> Iterator[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", "--continue-on-error", f"gs://{bucket_name}/*") except RuntimeError: # If no files were found and it fails, ignore the error. pass diff --git a/dataflow/custom-containers/miniconda/Dockerfile b/dataflow/custom-containers/miniconda/Dockerfile index bcc1eadc7f5..31e8c3b03f8 100644 --- a/dataflow/custom-containers/miniconda/Dockerfile +++ b/dataflow/custom-containers/miniconda/Dockerfile @@ -18,7 +18,7 @@ FROM continuumio/miniconda3:22.11.1-alpine AS builder # Create a virtual environment and make it standalone with conda-pack. # https://conda.github.io/conda-pack -RUN conda create -y -n env python=3.9 \ +RUN conda create -y -n env python=3.14 \ && conda install -y conda-pack \ && conda-pack -n env -o /tmp/env.tar \ && mkdir /opt/python \ @@ -31,7 +31,7 @@ FROM ubuntu:latest WORKDIR /pipeline # Set the entrypoint to Apache Beam SDK worker launcher. -COPY --from=apache/beam_python3.9_sdk:2.55.1 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] # Copy the python installation from the builder stage. diff --git a/dataflow/custom-containers/miniconda/noxfile_config.py b/dataflow/custom-containers/miniconda/noxfile_config.py index fb2bcbdea22..60b814e62b1 100644 --- a/dataflow/custom-containers/miniconda/noxfile_config.py +++ b/dataflow/custom-containers/miniconda/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ We're opting out of all Python versions except 3.9. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.10", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/dataflow/custom-containers/miniconda/requirements-test.txt b/dataflow/custom-containers/miniconda/requirements-test.txt index 2402ce8311b..e1be392249c 100644 --- a/dataflow/custom-containers/miniconda/requirements-test.txt +++ b/dataflow/custom-containers/miniconda/requirements-test.txt @@ -1,4 +1,4 @@ google-api-python-client==2.131.0 google-cloud-storage==2.9.0 pytest-xdist==3.3.0 -pytest==8.2.0 \ No newline at end of file +pytest==9.1.1 \ No newline at end of file diff --git a/dataflow/custom-containers/minimal/Dockerfile b/dataflow/custom-containers/minimal/Dockerfile index 2176aa76a81..cb113d33854 100644 --- a/dataflow/custom-containers/minimal/Dockerfile +++ b/dataflow/custom-containers/minimal/Dockerfile @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM python:3.9-slim +FROM python:3.14-slim WORKDIR /pipeline # Set the entrypoint to Apache Beam SDK worker launcher. -COPY --from=apache/beam_python3.9_sdk:2.55.1 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] # Install the requirements. diff --git a/dataflow/custom-containers/minimal/noxfile_config.py b/dataflow/custom-containers/minimal/noxfile_config.py index fb2bcbdea22..60b814e62b1 100644 --- a/dataflow/custom-containers/minimal/noxfile_config.py +++ b/dataflow/custom-containers/minimal/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ We're opting out of all Python versions except 3.9. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.10", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/dataflow/custom-containers/minimal/requirements-test.txt b/dataflow/custom-containers/minimal/requirements-test.txt index 2402ce8311b..e1be392249c 100644 --- a/dataflow/custom-containers/minimal/requirements-test.txt +++ b/dataflow/custom-containers/minimal/requirements-test.txt @@ -1,4 +1,4 @@ google-api-python-client==2.131.0 google-cloud-storage==2.9.0 pytest-xdist==3.3.0 -pytest==8.2.0 \ No newline at end of file +pytest==9.1.1 \ No newline at end of file diff --git a/dataflow/custom-containers/ubuntu/Dockerfile b/dataflow/custom-containers/ubuntu/Dockerfile index c35d23d9957..67e5d80e998 100644 --- a/dataflow/custom-containers/ubuntu/Dockerfile +++ b/dataflow/custom-containers/ubuntu/Dockerfile @@ -17,7 +17,7 @@ FROM ubuntu:focal WORKDIR /pipeline # Set the entrypoint to Apache Beam SDK worker launcher. -COPY --from=apache/beam_python3.8_sdk:2.40.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] # Install Python with pip, dev tools, distutils, and a C++ compiler. diff --git a/dataflow/custom-containers/ubuntu/noxfile_config.py b/dataflow/custom-containers/ubuntu/noxfile_config.py index fb2bcbdea22..60b814e62b1 100644 --- a/dataflow/custom-containers/ubuntu/noxfile_config.py +++ b/dataflow/custom-containers/ubuntu/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ We're opting out of all Python versions except 3.9. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.10", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/dataflow/custom-containers/ubuntu/requirements-test.txt b/dataflow/custom-containers/ubuntu/requirements-test.txt index 2402ce8311b..e1be392249c 100644 --- a/dataflow/custom-containers/ubuntu/requirements-test.txt +++ b/dataflow/custom-containers/ubuntu/requirements-test.txt @@ -1,4 +1,4 @@ google-api-python-client==2.131.0 google-cloud-storage==2.9.0 pytest-xdist==3.3.0 -pytest==8.2.0 \ No newline at end of file +pytest==9.1.1 \ No newline at end of file diff --git a/dataflow/encryption-keys/README.md b/dataflow/encryption-keys/README.md index 0545d63d321..036540108d8 100644 --- a/dataflow/encryption-keys/README.md +++ b/dataflow/encryption-keys/README.md @@ -25,7 +25,7 @@ Additionally, for this sample you need the following: ```sh export BUCKET=your-gcs-bucket - gsutil mb gs://$BUCKET + gcloud storage buckets create gs://$BUCKET ``` 1. [Create a symmetric key ring](https://cloud.google.com/kms/docs/creating-keys). @@ -174,10 +174,10 @@ To avoid incurring charges to your GCP account for the resources used: ```sh # Remove only the files created by this sample. -gsutil -m rm -rf "gs://$BUCKET/samples/dataflow/kms" +gcloud storage rm --recursive --continue-on-error "gs://$BUCKET/samples/dataflow/kms" # [optional] Remove the Cloud Storage bucket. -gsutil rb gs://$BUCKET +gcloud storage buckets delete gs://$BUCKET # Remove the BigQuery table. bq rm -f -t $PROJECT:$DATASET.$TABLE diff --git a/dataflow/encryption-keys/noxfile_config.py b/dataflow/encryption-keys/noxfile_config.py index bf456e51902..932bef266b6 100644 --- a/dataflow/encryption-keys/noxfile_config.py +++ b/dataflow/encryption-keys/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.6", "3.8", "3.9", "3.10", "3.11"], + "ignored_versions": ["3.8", "3.9", "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/dataflow/encryption-keys/requirements-test.txt b/dataflow/encryption-keys/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/dataflow/encryption-keys/requirements-test.txt +++ b/dataflow/encryption-keys/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/dataflow/extensible-templates/noxfile_config.py b/dataflow/extensible-templates/noxfile_config.py index d15bd45490f..3fb32a57d23 100644 --- a/dataflow/extensible-templates/noxfile_config.py +++ b/dataflow/extensible-templates/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.6", "3.7", "3.8", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/dataflow/extensible-templates/requirements-test.txt b/dataflow/extensible-templates/requirements-test.txt index a51578c86f2..ba4350c2c77 100644 --- a/dataflow/extensible-templates/requirements-test.txt +++ b/dataflow/extensible-templates/requirements-test.txt @@ -2,5 +2,5 @@ google-api-python-client==2.131.0 google-cloud-bigquery==3.27.0 google-cloud-storage==2.9.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pyyaml==6.0.2 \ No newline at end of file diff --git a/dataflow/flex-templates/getting_started/README.md b/dataflow/flex-templates/getting_started/README.md index d7ee5d38bd3..383fdca052e 100644 --- a/dataflow/flex-templates/getting_started/README.md +++ b/dataflow/flex-templates/getting_started/README.md @@ -9,7 +9,7 @@ Make sure you have followed the ```sh export BUCKET="your--bucket" -gsutil mb gs://$BUCKET +gcloud storage buckets create gs://$BUCKET ``` ## create an Artifact Registry repository @@ -51,4 +51,3 @@ gcloud dataflow flex-template run "flex-`date +%Y%m%d-%H%M%S`" \ For more information about building and running flex templates, see 📝 [Use Flex Templates](https://cloud.google.com/dataflow/docs/guides/templates/using-flex-templates). - diff --git a/dataflow/flex-templates/pipeline_with_dependencies/Dockerfile b/dataflow/flex-templates/pipeline_with_dependencies/Dockerfile index e85016b1411..0939e9e0728 100644 --- a/dataflow/flex-templates/pipeline_with_dependencies/Dockerfile +++ b/dataflow/flex-templates/pipeline_with_dependencies/Dockerfile @@ -24,14 +24,14 @@ # This Dockerfile illustrates how to use a custom base image when building # a custom contaier images for Dataflow. A 'slim' base image is smaller in size, # but does not include some preinstalled libraries, like google-cloud-debugger. -# To use a standard image, use apache/beam_python3.11_sdk:2.54.0 instead. +# To use a standard image, use apache/beam_python3.14_sdk:2.73.0 instead. # Use consistent versions of Python interpreter in the project. -FROM python:3.11-slim +FROM python:3.14-slim # Copy SDK entrypoint binary from Apache Beam image, which makes it possible to # use the image as SDK container image. If you explicitly depend on # apache-beam in setup.py, use the same version of Beam in both files. -COPY --from=apache/beam_python3.11_sdk:2.54.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam # Copy Flex Template launcher binary from the launcher image, which makes it # possible to use the image as a Flex Template base image. diff --git a/dataflow/flex-templates/pipeline_with_dependencies/README.md b/dataflow/flex-templates/pipeline_with_dependencies/README.md index 99385639297..79bbabf9761 100644 --- a/dataflow/flex-templates/pipeline_with_dependencies/README.md +++ b/dataflow/flex-templates/pipeline_with_dependencies/README.md @@ -73,7 +73,7 @@ rules. It is optional. export PROJECT="project-id" export BUCKET="your-bucket" export REGION="us-central1" -gsutil mb -p $PROJECT gs://$BUCKET +gcloud storage buckets create gs://$BUCKET --project=$PROJECT ``` ## Create an Artifact Registry repository @@ -165,7 +165,7 @@ gcloud dataflow flex-template run "flex-`date +%Y%m%d-%H%M%S`" \ After the pipeline finishes, use the following command to inspect the output: ```bash -gsutil cat gs://$BUCKET/output* +gcloud storage cat gs://$BUCKET/output* ``` ## Optional: Update the dependencies in the requirements file and rebuild the Docker images diff --git a/dataflow/flex-templates/pipeline_with_dependencies/noxfile_config.py b/dataflow/flex-templates/pipeline_with_dependencies/noxfile_config.py index 8df70c1108b..c3badedaaba 100644 --- a/dataflow/flex-templates/pipeline_with_dependencies/noxfile_config.py +++ b/dataflow/flex-templates/pipeline_with_dependencies/noxfile_config.py @@ -16,8 +16,8 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ We're opting out of all Python versions except 3.11. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], } diff --git a/dataflow/flex-templates/pipeline_with_dependencies/pyproject.toml b/dataflow/flex-templates/pipeline_with_dependencies/pyproject.toml index ecd0b27cecf..e3dc2b2f52c 100644 --- a/dataflow/flex-templates/pipeline_with_dependencies/pyproject.toml +++ b/dataflow/flex-templates/pipeline_with_dependencies/pyproject.toml @@ -20,6 +20,6 @@ build-backend = "setuptools.build_meta" name = "my_package" version = "0.1.0" dependencies = [ - "apache-beam[gcp]==2.54.0", # Must match the version in `Dockerfile``. + "apache-beam[gcp]==2.73.0", # Must match the version in `Dockerfile``. "pyfiglet", # This is the only non-Beam dependency of this pipeline. ] diff --git a/dataflow/flex-templates/pipeline_with_dependencies/requirements-test.txt b/dataflow/flex-templates/pipeline_with_dependencies/requirements-test.txt index 16c7c4dffed..f686d5a9428 100644 --- a/dataflow/flex-templates/pipeline_with_dependencies/requirements-test.txt +++ b/dataflow/flex-templates/pipeline_with_dependencies/requirements-test.txt @@ -17,5 +17,5 @@ google-api-python-client==2.87.0 google-cloud-storage==2.9.0 pytest-xdist==3.3.0 -pytest==7.0.1 +pytest==9.0.3 pyyaml==6.0 diff --git a/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt b/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt index eeed8f6f3ce..2f3e3fa8778 100644 --- a/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt +++ b/dataflow/flex-templates/pipeline_with_dependencies/requirements.txt @@ -1,103 +1,141 @@ -# Copyright 2024 Google LLC # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile --output-file=requirements.txt pyproject.toml # -apache-beam[gcp]==2.54.0 + +aiofiles==25.1.0 + # via cloud-sql-python-connector +aiohappyeyeballs==2.7.1 + # via aiohttp +aiohttp==3.14.1 + # via cloud-sql-python-connector +aiosignal==1.4.0 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.14.1 + # via + # google-genai + # httpx +apache-beam[gcp]==2.73.0 # via my_package (pyproject.toml) -attrs==23.2.0 +asn1crypto==1.5.1 + # via scramp +attrs==26.1.0 + # via aiohttp +beartype==0.22.9 + # via apache-beam +betterproto==2.0.0b6 + # via envoy-data-plane +cachetools==6.2.6 + # via apache-beam +certifi==2026.6.17 # via - # jsonschema - # referencing -cachetools==5.3.3 + # google-cloud-aiplatform + # httpcore + # httpx + # requests +cffi==2.1.0 + # via cryptography +charset-normalizer==3.4.8 + # via requests +cloud-sql-python-connector==1.20.4 + # via apache-beam +cryptography==47.0.0 # via # apache-beam + # cloud-sql-python-connector # google-auth -certifi==2024.7.4 - # via requests -charset-normalizer==3.3.2 - # via requests -cloudpickle==2.2.1 - # via apache-beam -crcmod==1.7 - # via apache-beam -deprecated==1.2.14 - # via google-cloud-spanner -dill==0.3.1.1 + # pyopenssl + # secretstorage +distro==1.9.0 + # via google-genai +dnspython==2.8.0 + # via + # cloud-sql-python-connector + # pymongo +docstring-parser==0.18.0 + # via google-cloud-aiplatform +envoy-data-plane==1.0.3 # via apache-beam -dnspython==2.6.1 - # via pymongo -docopt==0.6.2 - # via hdfs -fastavro==1.9.4 +fastavro==1.12.2 # via apache-beam -fasteners==0.19 +fasteners==0.20 # via # apache-beam # google-apitools -google-api-core[grpc]==2.17.1 +frozenlist==1.8.0 + # via + # aiohttp + # aiosignal +google-api-core[grpc]==2.31.0 # via # apache-beam # google-cloud-aiplatform # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable + # google-cloud-build # google-cloud-core # google-cloud-datastore # google-cloud-dlp + # google-cloud-kms # google-cloud-language + # google-cloud-monitoring # google-cloud-pubsub - # google-cloud-pubsublite # google-cloud-recommendations-ai # google-cloud-resource-manager + # google-cloud-secret-manager # google-cloud-spanner # google-cloud-storage # google-cloud-videointelligence # google-cloud-vision -google-apitools==0.5.31 +google-apitools==0.5.35 # via apache-beam -google-auth==2.28.1 +google-auth[pyopenssl,requests]==2.55.1 # via # apache-beam + # cloud-sql-python-connector # google-api-core # google-auth-httplib2 # google-cloud-aiplatform + # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-build # google-cloud-core + # google-cloud-datastore # google-cloud-dlp + # google-cloud-kms # google-cloud-language + # google-cloud-monitoring # google-cloud-pubsub # google-cloud-recommendations-ai # google-cloud-resource-manager + # google-cloud-secret-manager + # google-cloud-spanner # google-cloud-storage # google-cloud-videointelligence # google-cloud-vision -google-auth-httplib2==0.1.1 + # google-genai + # keyrings-google-artifactregistry-auth +google-auth-httplib2==0.2.1 # via apache-beam -google-cloud-aiplatform==1.42.1 +google-cloud-aiplatform==1.148.1 # via apache-beam -google-cloud-bigquery==3.17.2 +google-cloud-bigquery==3.42.1 # via # apache-beam # google-cloud-aiplatform -google-cloud-bigquery-storage==2.24.0 +google-cloud-bigquery-storage==2.39.0 + # via apache-beam +google-cloud-bigtable==2.40.0 # via apache-beam -google-cloud-bigtable==2.23.0 +google-cloud-build==3.38.0 # via apache-beam -google-cloud-core==2.4.1 +google-cloud-core==2.6.0 # via # apache-beam # google-cloud-bigquery @@ -105,209 +143,327 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-spanner # google-cloud-storage -google-cloud-datastore==2.19.0 +google-cloud-datastore==2.25.0 # via apache-beam -google-cloud-dlp==3.15.2 +google-cloud-dlp==3.38.0 # via apache-beam -google-cloud-language==2.13.2 +google-cloud-kms==3.14.0 # via apache-beam -google-cloud-pubsub==2.19.7 - # via - # apache-beam - # google-cloud-pubsublite -google-cloud-pubsublite==1.9.0 +google-cloud-language==2.21.0 # via apache-beam -google-cloud-recommendations-ai==0.10.9 +google-cloud-monitoring==2.31.0 + # via google-cloud-spanner +google-cloud-pubsub==2.39.0 + # via apache-beam +google-cloud-recommendations-ai==0.10.18 # via apache-beam -google-cloud-resource-manager==1.12.2 +google-cloud-resource-manager==1.18.0 # via google-cloud-aiplatform -google-cloud-spanner==3.42.0 +google-cloud-secret-manager==2.29.0 + # via apache-beam +google-cloud-spanner==3.69.0 # via apache-beam -google-cloud-storage==2.14.0 +google-cloud-storage==2.19.0 # via # apache-beam # google-cloud-aiplatform -google-cloud-videointelligence==2.13.2 +google-cloud-videointelligence==2.20.0 # via apache-beam -google-cloud-vision==3.7.1 +google-cloud-vision==3.15.0 # via apache-beam -google-crc32c==1.5.0 +google-crc32c==1.8.0 # via + # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.0 +google-genai==1.75.0 + # via google-cloud-aiplatform +google-resumable-media==2.10.0 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.62.0 +googleapis-common-protos[grpc]==1.75.0 # via # google-api-core # grpc-google-iam-v1 # grpcio-status -grpc-google-iam-v1==0.13.0 +grpc-google-iam-v1==0.14.4 # via # google-cloud-bigtable + # google-cloud-build + # google-cloud-kms # google-cloud-pubsub # google-cloud-resource-manager + # google-cloud-secret-manager # google-cloud-spanner grpc-interceptor==0.15.4 # via google-cloud-spanner -grpcio==1.62.0 +grpcio==1.82.0 # via # apache-beam # google-api-core + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-build + # google-cloud-datastore + # google-cloud-dlp + # google-cloud-kms + # google-cloud-language + # google-cloud-monitoring # google-cloud-pubsub - # google-cloud-pubsublite + # google-cloud-resource-manager + # google-cloud-secret-manager + # google-cloud-spanner + # google-cloud-videointelligence + # google-cloud-vision # googleapis-common-protos # grpc-google-iam-v1 # grpc-interceptor # grpcio-status -grpcio-status==1.62.0 + # grpcio-tools +grpcio-status==1.82.0 # via # google-api-core # google-cloud-pubsub - # google-cloud-pubsublite -hdfs==2.7.3 - # via apache-beam -httplib2==0.22.0 +grpcio-tools==1.82.0 + # via envoy-data-plane +grpclib==0.4.9 + # via betterproto +h11==0.16.0 + # via httpcore +h2==4.3.0 + # via grpclib +hpack==4.2.0 + # via h2 +httpcore==1.0.9 + # via httpx +httplib2==0.31.2 # via # apache-beam # google-apitools # google-auth-httplib2 # oauth2client -idna==3.7 - # via requests -js2py==0.74 - # via apache-beam -jsonpickle==3.0.3 +httpx==0.28.1 + # via google-genai +hyperframe==6.1.0 + # via h2 +idna==3.18 + # via + # anyio + # httpx + # requests + # yarl +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.5.0 + # via keyring +jeepney==0.9.0 + # via + # keyring + # secretstorage +jsonpickle==3.4.2 # via apache-beam -jsonschema==4.21.1 +keyring==25.7.0 + # via keyrings-google-artifactregistry-auth +keyrings-google-artifactregistry-auth==1.1.2 # via apache-beam -jsonschema-specifications==2023.12.1 - # via jsonschema -numpy==1.24.4 +mmh3==5.2.1 + # via google-cloud-spanner +more-itertools==11.1.0 # via - # apache-beam - # pyarrow - # shapely + # jaraco-classes + # jaraco-functools +multidict==6.7.1 + # via + # aiohttp + # grpclib + # yarl +numpy==2.4.6 + # via apache-beam oauth2client==4.1.3 # via google-apitools -objsize==0.7.0 +objsize==0.7.1 # via apache-beam -orjson==3.9.15 +opentelemetry-api==1.43.0 + # via + # google-cloud-pubsub + # google-cloud-spanner + # opentelemetry-resourcedetector-gcp + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-resourcedetector-gcp==1.12.0a0 + # via google-cloud-spanner +opentelemetry-sdk==1.43.0 + # via + # google-cloud-pubsub + # google-cloud-spanner + # opentelemetry-resourcedetector-gcp +opentelemetry-semantic-conventions==0.64b0 + # via + # google-cloud-spanner + # opentelemetry-sdk +orjson==3.11.9 # via apache-beam -overrides==7.7.0 - # via google-cloud-pubsublite -packaging==23.2 +packaging==26.2 # via # apache-beam # google-cloud-aiplatform # google-cloud-bigquery -proto-plus==1.23.0 +pg8000==1.31.5 + # via apache-beam +pillow==12.3.0 + # via apache-beam +pluggy==1.6.0 + # via keyrings-google-artifactregistry-auth +propcache==0.5.2 + # via + # aiohttp + # yarl +proto-plus==1.28.0 # via # apache-beam + # google-api-core # google-cloud-aiplatform # google-cloud-bigquery-storage # google-cloud-bigtable + # google-cloud-build # google-cloud-datastore # google-cloud-dlp + # google-cloud-kms # google-cloud-language + # google-cloud-monitoring # google-cloud-pubsub # google-cloud-recommendations-ai # google-cloud-resource-manager + # google-cloud-secret-manager # google-cloud-spanner # google-cloud-videointelligence # google-cloud-vision -protobuf==4.25.8 +protobuf==6.33.6 # via # apache-beam # google-api-core # google-cloud-aiplatform # google-cloud-bigquery-storage # google-cloud-bigtable + # google-cloud-build # google-cloud-datastore # google-cloud-dlp + # google-cloud-kms # google-cloud-language + # google-cloud-monitoring # google-cloud-pubsub # google-cloud-recommendations-ai # google-cloud-resource-manager + # google-cloud-secret-manager # google-cloud-spanner # google-cloud-videointelligence # google-cloud-vision # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status + # grpcio-tools # proto-plus -pyarrow==14.0.2 +pyarrow==23.0.1 # via apache-beam -pyarrow-hotfix==0.6 +pyarrow-hotfix==0.7 # via apache-beam -pyasn1==0.5.1 +pyasn1==0.6.3 # via # oauth2client # pyasn1-modules # rsa -pyasn1-modules==0.3.0 +pyasn1-modules==0.4.2 # via # google-auth # oauth2client -pydot==1.4.2 - # via apache-beam -pyfiglet==1.0.2 +pycparser==3.0 + # via cffi +pydantic==2.13.4 + # via + # google-cloud-aiplatform + # google-genai +pydantic-core==2.46.4 + # via pydantic +pyfiglet==1.0.4 # via my_package (pyproject.toml) -pyjsparser==2.7.1 - # via js2py -pymongo==4.6.3 +pymongo==4.17.0 # via apache-beam -pyparsing==3.1.1 - # via - # httplib2 - # pydot -python-dateutil==2.8.2 +pymysql==1.2.0 + # via apache-beam +pyopenssl==26.2.0 + # via google-auth +pyparsing==3.3.2 + # via httplib2 +python-dateutil==2.9.0.post0 # via # apache-beam + # betterproto # google-cloud-bigquery -pytz==2024.1 + # pg8000 +python-tds==1.17.1 # via apache-beam -referencing==0.33.0 - # via - # jsonschema - # jsonschema-specifications -regex==2023.12.25 +pytz==2026.2 + # via apache-beam +pyyaml==6.0.3 # via apache-beam -requests==2.31.0 +regex==2026.6.28 + # via apache-beam +requests==2.34.2 # via # apache-beam + # cloud-sql-python-connector # google-api-core + # google-auth # google-cloud-bigquery # google-cloud-storage - # hdfs -rpds-py==0.18.0 - # via - # jsonschema - # referencing -rsa==4.9 - # via - # google-auth - # oauth2client -shapely==2.0.3 - # via google-cloud-aiplatform -six==1.16.0 + # google-genai + # keyrings-google-artifactregistry-auth + # opentelemetry-resourcedetector-gcp +rsa==4.9.1 + # via oauth2client +scramp==1.4.12 + # via pg8000 +secretstorage==3.5.0 + # via keyring +six==1.17.0 # via # google-apitools - # hdfs - # js2py # oauth2client # python-dateutil -sqlparse==0.5.0 - # via google-cloud-spanner -typing-extensions==4.10.0 +sniffio==1.3.1 + # via google-genai +sortedcontainers==2.4.0 # via apache-beam -tzlocal==5.2 - # via js2py -urllib3==2.5.0 +sqlparse==0.5.5 + # via google-cloud-spanner +tenacity==9.1.4 + # via google-genai +typing-extensions==4.16.0 + # via + # apache-beam + # google-cloud-aiplatform + # google-genai + # grpcio + # opentelemetry-api + # opentelemetry-resourcedetector-gcp + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.2 + # via pydantic +urllib3==2.7.0 # via requests -wrapt==1.16.0 - # via deprecated -zstandard==0.22.0 +websockets==16.0 + # via google-genai +yarl==1.24.2 + # via aiohttp +zstandard==0.25.0 # via apache-beam + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/dataflow/flex-templates/streaming_beam/README.md b/dataflow/flex-templates/streaming_beam/README.md index 66d891ce526..2a21e44220b 100644 --- a/dataflow/flex-templates/streaming_beam/README.md +++ b/dataflow/flex-templates/streaming_beam/README.md @@ -25,7 +25,7 @@ Additionally, for this sample you need the following: ```sh export BUCKET="your-gcs-bucket" - gsutil mb gs://$BUCKET + gcloud storage buckets create gs://$BUCKET ``` 1. Create a @@ -231,7 +231,7 @@ The following sections describe how to delete or turn off these resources. 1. Delete the template spec file from Cloud Storage. ```sh - gsutil rm $TEMPLATE_PATH + gcloud storage rm $TEMPLATE_PATH ``` 1. Delete the Flex Template container image from Container Registry. @@ -277,7 +277,7 @@ The following sections describe how to delete or turn off these resources. > These objects cannot be recovered. > > ```sh - > gsutil rm -r gs://$BUCKET + > gcloud storage rm --recursive gs://$BUCKET > ``` ## Limitations diff --git a/dataflow/flex-templates/streaming_beam/requirements-test.txt b/dataflow/flex-templates/streaming_beam/requirements-test.txt index fcbae6b06bb..85879ed83b3 100644 --- a/dataflow/flex-templates/streaming_beam/requirements-test.txt +++ b/dataflow/flex-templates/streaming_beam/requirements-test.txt @@ -1,5 +1,5 @@ google-api-python-client==2.131.0 google-cloud-storage==2.9.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.1.1 pyyaml==6.0.2 \ No newline at end of file diff --git a/dataflow/gemma-flex-template/Dockerfile b/dataflow/gemma-flex-template/Dockerfile index 284474e9759..fb2c52d27d6 100644 --- a/dataflow/gemma-flex-template/Dockerfile +++ b/dataflow/gemma-flex-template/Dockerfile @@ -30,11 +30,11 @@ RUN pip install --no-cache-dir --upgrade pip \ # Copy SDK entrypoint binary from Apache Beam image, which makes it possible to # use the image as SDK container image. # The Beam version should match the version specified in requirements.txt -COPY --from=apache/beam_python3.10_sdk:2.62.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam # Copy Flex Template launcher binary from the launcher image, which makes it # possible to use the image as a Flex Template base image. -COPY --from=gcr.io/dataflow-templates-base/python310-template-launcher-base:latest /opt/google/dataflow/python_template_launcher /opt/google/dataflow/python_template_launcher +COPY --from=gcr.io/dataflow-templates-base/python314-template-launcher-base:latest /opt/google/dataflow/python_template_launcher /opt/google/dataflow/python_template_launcher # Copy the model directory downloaded from Kaggle and the pipeline code. COPY pytorch_model pytorch_model diff --git a/dataflow/gemma-flex-template/README.md b/dataflow/gemma-flex-template/README.md index 8ade42a8c46..0e082cc2bb7 100644 --- a/dataflow/gemma-flex-template/README.md +++ b/dataflow/gemma-flex-template/README.md @@ -43,7 +43,7 @@ Click [here to create a GCS bucket](https://console.cloud.google.com/storage/cre ```sh export GCS_BUCKET="your--bucket" -gsutil mb gs://$GCS_BUCKET +gcloud storage buckets create gs://$GCS_BUCKET ``` Make sure your GCS bucket name does __not__ include the `gs://` prefix diff --git a/dataflow/gemma-flex-template/e2e_test.py b/dataflow/gemma-flex-template/e2e_test.py index f95f78ec089..be281a9c984 100644 --- a/dataflow/gemma-flex-template/e2e_test.py +++ b/dataflow/gemma-flex-template/e2e_test.py @@ -92,7 +92,7 @@ def responses_subscription( @pytest.fixture(scope="session") def flex_template_image(utils: Utils) -> str: - conftest.run_cmd("gsutil", "cp", "-r", GEMMA_GCS, ".") + conftest.run_cmd("gcloud", "storage", "cp", "--recursive", GEMMA_GCS, ".") yield from utils.cloud_build_submit(NAME) diff --git a/dataflow/gemma-flex-template/noxfile_config.py b/dataflow/gemma-flex-template/noxfile_config.py index 7e6ba7ba31b..35321dbbdea 100644 --- a/dataflow/gemma-flex-template/noxfile_config.py +++ b/dataflow/gemma-flex-template/noxfile_config.py @@ -16,10 +16,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # Opting out of all Python versions except 3.10. # The Python version used is defined by the Dockerfile and the job # submission enviornment must match. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], "envs": { "PYTHONPATH": ".." }, diff --git a/dataflow/gemma-flex-template/requirements-test.txt b/dataflow/gemma-flex-template/requirements-test.txt index 5e6dcfc99aa..6138ec5ac4e 100644 --- a/dataflow/gemma-flex-template/requirements-test.txt +++ b/dataflow/gemma-flex-template/requirements-test.txt @@ -1,7 +1,7 @@ -google-cloud-aiplatform==1.62.0 +google-cloud-aiplatform==1.158.0 google-cloud-dataflow-client==0.8.14 google-cloud-pubsub==2.28.0 google-cloud-storage==2.18.2 -pytest==8.3.2 +pytest==9.0.3; python_version >= "3.10" pytest-timeout==2.3.1 pyyaml==6.0.2 diff --git a/dataflow/gemma/Dockerfile b/dataflow/gemma/Dockerfile index d66c298e6eb..ebe142aa64d 100644 --- a/dataflow/gemma/Dockerfile +++ b/dataflow/gemma/Dockerfile @@ -15,7 +15,7 @@ # This uses Ubuntu with Python 3.11 # You can check the Python version for a given tensorflow # container at https://hub.docker.com/r/tensorflow/tensorflow/tags -ARG SERVING_BUILD_IMAGE=tensorflow/tensorflow:2.16.1-gpu +ARG SERVING_BUILD_IMAGE=tensorflow/tensorflow:2.20.0-gpu FROM ${SERVING_BUILD_IMAGE} @@ -29,7 +29,7 @@ RUN pip install --upgrade --no-cache-dir pip \ && pip install --no-cache-dir -r requirements.txt # Copy files from official SDK image, including script/dependencies. -COPY --from=apache/beam_python3.11_sdk:2.54.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.11_sdk:2.74.0 /opt/apache/beam /opt/apache/beam # Copy the model directory downloaded from Kaggle and the pipeline code. COPY gemma_2b gemma_2B diff --git a/dataflow/gemma/custom_model_gemma.py b/dataflow/gemma/custom_model_gemma.py index fbf0b975057..d2b556a0247 100644 --- a/dataflow/gemma/custom_model_gemma.py +++ b/dataflow/gemma/custom_model_gemma.py @@ -13,13 +13,11 @@ # limitations under the License. from collections.abc import Iterable, Sequence - -import logging - from typing import Any from typing import Optional import apache_beam as beam + from apache_beam.ml.inference import utils from apache_beam.ml.inference.base import ModelHandler from apache_beam.ml.inference.base import PredictionResult @@ -27,15 +25,18 @@ from apache_beam.options.pipeline_options import PipelineOptions import keras_nlp + from keras_nlp.src.models.gemma.gemma_causal_lm import GemmaCausalLM +import logging + class GemmaModelHandler(ModelHandler[str, PredictionResult, GemmaCausalLM]): def __init__( self, model_name: str = "gemma_2B", ): - """ Implementation of the ModelHandler interface for Gemma using text as input. + """Implementation of the ModelHandler interface for Gemma using text as input. Example Usage:: @@ -48,7 +49,7 @@ def __init__( self._env_vars = {} def share_model_across_processes(self) -> bool: - """ Indicates if the model should be loaded once-per-VM rather than + """Indicates if the model should be loaded once-per-VM rather than once-per-worker-process on a VM. Because Gemma is a large language model, this will always return True to avoid OOM errors. """ @@ -62,7 +63,7 @@ def run_inference( self, batch: Sequence[str], model: GemmaCausalLM, - inference_args: Optional[dict[str, Any]] = None + inference_args: Optional[dict[str, Any]] = None, ) -> Iterable[PredictionResult]: """Runs inferences on a batch of text strings. @@ -85,7 +86,8 @@ def run_inference( class FormatOutput(beam.DoFn): def process(self, element, *args, **kwargs): yield "Input: {input}, Output: {output}".format( - input=element.example, output=element.inference) + input=element.example, output=element.inference + ) if __name__ == "__main__": @@ -119,13 +121,16 @@ def process(self, element, *args, **kwargs): pipeline = beam.Pipeline(options=beam_options) _ = ( - pipeline | "Read Topic" >> - beam.io.ReadFromPubSub(subscription=args.messages_subscription) + pipeline + | "Read Topic" + >> beam.io.ReadFromPubSub(subscription=args.messages_subscription) | "Parse" >> beam.Map(lambda x: x.decode("utf-8")) - | "RunInference-Gemma" >> RunInference( + | "RunInference-Gemma" + >> RunInference( GemmaModelHandler(args.model_path) ) # Send the prompts to the model and get responses. | "Format Output" >> beam.ParDo(FormatOutput()) # Format the output. - | "Publish Result" >> - beam.io.gcp.pubsub.WriteStringsToPubSub(topic=args.responses_topic)) + | "Publish Result" + >> beam.io.gcp.pubsub.WriteStringsToPubSub(topic=args.responses_topic) + ) pipeline.run() diff --git a/dataflow/gemma/e2e_test.py b/dataflow/gemma/e2e_test.py index e2510716f4b..fdb187d4b91 100644 --- a/dataflow/gemma/e2e_test.py +++ b/dataflow/gemma/e2e_test.py @@ -34,11 +34,12 @@ OPTION B: Run tests with nox pip install nox - nox -s py-3.10 + nox -s py-3.11 -NOTE: For the tests to find the conftest in the testing infrastructure, +TODO(developer): For the tests to find the conftest in the testing infrastructure, add the PYTHONPATH to the "env" in your noxfile_config.py file. """ + from collections.abc import Callable, Iterator import conftest # python-docs-samples/dataflow/conftest.py @@ -46,7 +47,10 @@ import pytest -DATAFLOW_MACHINE_TYPE = "g2-standard-4" +DATAFLOW_MACHINE_TYPE = "g2-standard-8" +# TODO(developer): For local testing, ensure the 'gemma_2b_en' directory is uploaded +# to a GCS bucket you manage. Update the constant below to point to +# the root path of this uploaded directory (e.g., 'gs://your-bucket-name/path/to/gemma_2b'). GEMMA_GCS = "gs://perm-dataflow-gemma-example-testdata/gemma_2b" NAME = "dataflow/gemma/streaming" @@ -60,7 +64,7 @@ def test_name() -> str: @pytest.fixture(scope="session") def container_image(utils: Utils) -> str: # Copy Gemma onto the local environment - conftest.run_cmd("gsutil", "cp", "-r", GEMMA_GCS, ".") + conftest.run_cmd("gcloud", "storage", "cp", "--recursive", GEMMA_GCS, ".") yield from utils.cloud_build_submit(NAME) @@ -70,8 +74,9 @@ def messages_topic(pubsub_topic: Callable[[str], str]) -> str: @pytest.fixture(scope="session") -def messages_subscription(pubsub_subscription: Callable[[str, str], str], - messages_topic: str) -> str: +def messages_subscription( + pubsub_subscription: Callable[[str, str], str], messages_topic: str +) -> str: return pubsub_subscription("messages", messages_topic) @@ -81,20 +86,21 @@ def responses_topic(pubsub_topic: Callable[[str], str]) -> str: @pytest.fixture(scope="session") -def responses_subscription(pubsub_subscription: Callable[[str, str], str], - responses_topic: str) -> str: +def responses_subscription( + pubsub_subscription: Callable[[str, str], str], responses_topic: str +) -> str: return pubsub_subscription("responses", responses_topic) @pytest.fixture(scope="session") def dataflow_job( - project: str, - bucket_name: str, - location: str, - unique_name: str, - container_image: str, - messages_subscription: str, - responses_topic: str, + project: str, + bucket_name: str, + location: str, + unique_name: str, + container_image: str, + messages_subscription: str, + responses_topic: str, ) -> Iterator[str]: # Launch the streaming Dataflow pipeline. conftest.run_cmd( @@ -108,6 +114,7 @@ def dataflow_job( f"--temp_location=gs://{bucket_name}/temp", f"--region={location}", f"--machine_type={DATAFLOW_MACHINE_TYPE}", + "--disk_size_gb=100", f"--sdk_container_image=gcr.io/{project}/{container_image}", "--dataflow_service_options=worker_accelerator=type:nvidia-l4;count:1;install-nvidia-driver:5xx", "--requirements_cache=skip", @@ -127,20 +134,18 @@ def dataflow_job( @pytest.mark.timeout(3600) def test_pipeline_dataflow( - project: str, - location: str, - dataflow_job: str, - messages_topic: str, - responses_subscription: str, + project: str, + location: str, + dataflow_job: str, + messages_topic: str, + responses_subscription: str, ) -> None: print(f"Waiting for the Dataflow workers to start: {dataflow_job}") conftest.wait_until( - lambda: conftest.dataflow_num_workers(project, location, dataflow_job) - > 0, + lambda: conftest.dataflow_num_workers(project, location, dataflow_job) > 0, "workers are running", ) - num_workers = conftest.dataflow_num_workers(project, location, - dataflow_job) + num_workers = conftest.dataflow_num_workers(project, location, dataflow_job) print(f"Dataflow job num_workers: {num_workers}") messages = ["This is a test for a Python sample."] diff --git a/dataflow/gemma/noxfile_config.py b/dataflow/gemma/noxfile_config.py index 7b3b1b9ebf6..1706c31b186 100644 --- a/dataflow/gemma/noxfile_config.py +++ b/dataflow/gemma/noxfile_config.py @@ -16,11 +16,9 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # Opting out of all Python versions except 3.11. # The Python version used is defined by the Dockerfile and the job # submission enviornment must match. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.12", "3.13"], - "envs": { - "PYTHONPATH": ".." - }, + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.11) + "ignored_versions": ["3.8", "3.9", "3.10", "3.12", "3.13", "3.14"], + "envs": {"PYTHONPATH": ".."}, } diff --git a/dataflow/gemma/requirements-test.txt b/dataflow/gemma/requirements-test.txt index 511d704b396..91653a8a423 100644 --- a/dataflow/gemma/requirements-test.txt +++ b/dataflow/gemma/requirements-test.txt @@ -1,5 +1,5 @@ -google-cloud-aiplatform==1.49.0 -google-cloud-dataflow-client==0.8.10 -google-cloud-storage==2.16.0 -pytest==7.4.0 -pytest-timeout==2.3.1 \ No newline at end of file +google-cloud-aiplatform==1.158.0 +google-cloud-dataflow-client==0.14.0 +google-cloud-storage==3.12.0 +pytest==9.0.3 +pytest-timeout==2.4.0 diff --git a/dataflow/gemma/requirements.txt b/dataflow/gemma/requirements.txt index 76fc60632ee..183b6016d7d 100644 --- a/dataflow/gemma/requirements.txt +++ b/dataflow/gemma/requirements.txt @@ -1,4 +1,5 @@ -apache_beam[gcp]==2.54.0 -protobuf==4.25.0 -keras_nlp==0.8.2 -keras==3.0.5 \ No newline at end of file +protobuf==6.33.6 +apache_beam[gcp]==2.74.0 +keras==3.14.1 +keras_nlp==0.29.1 +pyOpenSSL==26.3.0 diff --git a/dataflow/gpu-examples/pytorch-minimal/Dockerfile b/dataflow/gpu-examples/pytorch-minimal/Dockerfile index f86d8bb388f..df3903a36ed 100644 --- a/dataflow/gpu-examples/pytorch-minimal/Dockerfile +++ b/dataflow/gpu-examples/pytorch-minimal/Dockerfile @@ -27,5 +27,5 @@ RUN pip install --no-cache-dir --upgrade pip \ && pip check # Set the entrypoint to Apache Beam SDK worker launcher. -COPY --from=apache/beam_python3.10_sdk:2.62.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] diff --git a/dataflow/gpu-examples/pytorch-minimal/noxfile_config.py b/dataflow/gpu-examples/pytorch-minimal/noxfile_config.py index 99b1fb47b8e..ac06c5f8740 100644 --- a/dataflow/gpu-examples/pytorch-minimal/noxfile_config.py +++ b/dataflow/gpu-examples/pytorch-minimal/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ We're opting out of all Python versions except 3.10. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/dataflow/gpu-examples/pytorch-minimal/requirements-test.txt b/dataflow/gpu-examples/pytorch-minimal/requirements-test.txt index cf8059020cf..12743ff3576 100644 --- a/dataflow/gpu-examples/pytorch-minimal/requirements-test.txt +++ b/dataflow/gpu-examples/pytorch-minimal/requirements-test.txt @@ -1,4 +1,4 @@ google-api-python-client==2.131.0 google-cloud-storage==2.9.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/dataflow/gpu-examples/tensorflow-landsat-prime/Dockerfile b/dataflow/gpu-examples/tensorflow-landsat-prime/Dockerfile index a506a8727a7..2cabbc313ac 100644 --- a/dataflow/gpu-examples/tensorflow-landsat-prime/Dockerfile +++ b/dataflow/gpu-examples/tensorflow-landsat-prime/Dockerfile @@ -15,7 +15,7 @@ # NVIDIA CUDA container: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda # Supported NVIDIA images: https://gitlab.com/nvidia/container-images/cuda/blob/master/doc/supported-tags.md # TensorFlow/CUDA compatibility: https://www.tensorflow.org/install/source#gpu -FROM nvcr.io/nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 +FROM nvcr.io/nvidia/cuda:12.5.1-cudnn-runtime-ubuntu22.04 WORKDIR /pipeline @@ -25,9 +25,8 @@ COPY *.py ./ RUN apt-get update \ # Install Python and other system dependencies. && apt-get install -y --no-install-recommends \ - curl g++ python3.10-dev python3.10-venv python3-distutils \ && rm -rf /var/lib/apt/lists/* \ - && update-alternatives --install /usr/bin/python python /usr/bin/python3.10 10 \ + && update-alternatives --install /usr/bin/python python /usr/bin/python3.13 10 \ && curl https://bootstrap.pypa.io/get-pip.py | python \ # Install the pipeline requirements. && pip install --no-cache-dir -r requirements.txt \ @@ -35,5 +34,5 @@ RUN apt-get update \ # Set the entrypoint to Apache Beam SDK worker launcher. # Check this matches the apache-beam version in the requirements.txt -COPY --from=apache/beam_python3.10_sdk:2.62.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.13_sdk:2.74.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] diff --git a/dataflow/gpu-examples/tensorflow-landsat-prime/noxfile_config.py b/dataflow/gpu-examples/tensorflow-landsat-prime/noxfile_config.py index 376ea30e3b6..ca12a452f5b 100644 --- a/dataflow/gpu-examples/tensorflow-landsat-prime/noxfile_config.py +++ b/dataflow/gpu-examples/tensorflow-landsat-prime/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ Test only on Python 3.10. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/dataflow/gpu-examples/tensorflow-landsat-prime/requirements-test.txt b/dataflow/gpu-examples/tensorflow-landsat-prime/requirements-test.txt index c1f8e786b49..3b2567ab391 100644 --- a/dataflow/gpu-examples/tensorflow-landsat-prime/requirements-test.txt +++ b/dataflow/gpu-examples/tensorflow-landsat-prime/requirements-test.txt @@ -1,3 +1,3 @@ google-api-python-client==2.87.0 google-cloud-storage==2.9.0 -pytest==8.2.0 +pytest==9.0.3 diff --git a/dataflow/gpu-examples/tensorflow-landsat-prime/requirements.txt b/dataflow/gpu-examples/tensorflow-landsat-prime/requirements.txt index 69a05b307ac..0c0a496e3ed 100644 --- a/dataflow/gpu-examples/tensorflow-landsat-prime/requirements.txt +++ b/dataflow/gpu-examples/tensorflow-landsat-prime/requirements.txt @@ -1,4 +1,4 @@ -Pillow==10.4.0 -apache-beam[gcp]==2.58.1 +pillow==12.3.0; python_version >= "3.10" +apache-beam[gcp]==2.74.0 rasterio==1.3.10 -tensorflow==2.12.0 # Check TensorFlow/CUDA compatibility with Dockerfile: https://www.tensorflow.org/install/source#gpu +tensorflow==2.21.0 # Check TensorFlow/CUDA compatibility with Dockerfile: https://www.tensorflow.org/install/source#gpu diff --git a/dataflow/gpu-examples/tensorflow-landsat/Dockerfile b/dataflow/gpu-examples/tensorflow-landsat/Dockerfile index 39a836fdb0b..a0a685366be 100644 --- a/dataflow/gpu-examples/tensorflow-landsat/Dockerfile +++ b/dataflow/gpu-examples/tensorflow-landsat/Dockerfile @@ -15,7 +15,7 @@ # NVIDIA CUDA container: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda # Supported NVIDIA images: https://gitlab.com/nvidia/container-images/cuda/blob/master/doc/supported-tags.md # TensorFlow/CUDA compatibility: https://www.tensorflow.org/install/source#gpu -FROM nvcr.io/nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 +FROM nvcr.io/nvidia/cuda:12.5.1-cudnn-runtime-ubuntu22.04 WORKDIR /pipeline @@ -25,9 +25,8 @@ COPY *.py ./ RUN apt-get update \ # Install Python and other system dependencies. && apt-get install -y --no-install-recommends \ - curl g++ python3.10-dev python3.10-venv python3-distutils \ && rm -rf /var/lib/apt/lists/* \ - && update-alternatives --install /usr/bin/python python /usr/bin/python3.10 10 \ + && update-alternatives --install /usr/bin/python python /usr/bin/python3.13 10 \ && curl https://bootstrap.pypa.io/get-pip.py | python \ # Install the pipeline requirements. && pip install --no-cache-dir -r requirements.txt \ @@ -35,5 +34,5 @@ RUN apt-get update \ # Set the entrypoint to Apache Beam SDK worker launcher. # Check this matches the apache-beam version in the requirements.txt -COPY --from=apache/beam_python3.10_sdk:2.62.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.13_sdk:2.74.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] diff --git a/dataflow/gpu-examples/tensorflow-landsat/noxfile_config.py b/dataflow/gpu-examples/tensorflow-landsat/noxfile_config.py index baf97789883..33c259f5bef 100644 --- a/dataflow/gpu-examples/tensorflow-landsat/noxfile_config.py +++ b/dataflow/gpu-examples/tensorflow-landsat/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ Test only on Python 3.10. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/dataflow/gpu-examples/tensorflow-landsat/requirements-test.txt b/dataflow/gpu-examples/tensorflow-landsat/requirements-test.txt index c1f8e786b49..3b2567ab391 100644 --- a/dataflow/gpu-examples/tensorflow-landsat/requirements-test.txt +++ b/dataflow/gpu-examples/tensorflow-landsat/requirements-test.txt @@ -1,3 +1,3 @@ google-api-python-client==2.87.0 google-cloud-storage==2.9.0 -pytest==8.2.0 +pytest==9.0.3 diff --git a/dataflow/gpu-examples/tensorflow-landsat/requirements.txt b/dataflow/gpu-examples/tensorflow-landsat/requirements.txt index 69a05b307ac..0c0a496e3ed 100644 --- a/dataflow/gpu-examples/tensorflow-landsat/requirements.txt +++ b/dataflow/gpu-examples/tensorflow-landsat/requirements.txt @@ -1,4 +1,4 @@ -Pillow==10.4.0 -apache-beam[gcp]==2.58.1 +pillow==12.3.0; python_version >= "3.10" +apache-beam[gcp]==2.74.0 rasterio==1.3.10 -tensorflow==2.12.0 # Check TensorFlow/CUDA compatibility with Dockerfile: https://www.tensorflow.org/install/source#gpu +tensorflow==2.21.0 # Check TensorFlow/CUDA compatibility with Dockerfile: https://www.tensorflow.org/install/source#gpu diff --git a/dataflow/gpu-examples/tensorflow-minimal/Dockerfile b/dataflow/gpu-examples/tensorflow-minimal/Dockerfile index e5f79f6e4ad..a0a685366be 100644 --- a/dataflow/gpu-examples/tensorflow-minimal/Dockerfile +++ b/dataflow/gpu-examples/tensorflow-minimal/Dockerfile @@ -15,7 +15,7 @@ # NVIDIA CUDA container: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda # Supported NVIDIA images: https://gitlab.com/nvidia/container-images/cuda/blob/master/doc/supported-tags.md # TensorFlow/CUDA compatibility: https://www.tensorflow.org/install/source#gpu -FROM nvcr.io/nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 +FROM nvcr.io/nvidia/cuda:12.5.1-cudnn-runtime-ubuntu22.04 WORKDIR /pipeline @@ -25,9 +25,8 @@ COPY *.py ./ RUN apt-get update \ # Install Python and other system dependencies. && apt-get install -y --no-install-recommends \ - curl g++ python3.10-dev python3.10-venv python3-distutils \ && rm -rf /var/lib/apt/lists/* \ - && update-alternatives --install /usr/bin/python python /usr/bin/python3.10 10 \ + && update-alternatives --install /usr/bin/python python /usr/bin/python3.13 10 \ && curl https://bootstrap.pypa.io/get-pip.py | python \ # Install the pipeline requirements. && pip install --no-cache-dir -r requirements.txt \ @@ -35,5 +34,5 @@ RUN apt-get update \ # Set the entrypoint to Apache Beam SDK worker launcher. # Check this matches the apache-beam version in the requirements.txt -COPY --from=apache/beam_python3.10_sdk:2.62.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.13_sdk:2.74.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] diff --git a/dataflow/gpu-examples/tensorflow-minimal/noxfile_config.py b/dataflow/gpu-examples/tensorflow-minimal/noxfile_config.py index baf97789883..33c259f5bef 100644 --- a/dataflow/gpu-examples/tensorflow-minimal/noxfile_config.py +++ b/dataflow/gpu-examples/tensorflow-minimal/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ Test only on Python 3.10. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/dataflow/gpu-examples/tensorflow-minimal/requirements-test.txt b/dataflow/gpu-examples/tensorflow-minimal/requirements-test.txt index e5602fb507b..d12d277c292 100644 --- a/dataflow/gpu-examples/tensorflow-minimal/requirements-test.txt +++ b/dataflow/gpu-examples/tensorflow-minimal/requirements-test.txt @@ -1,3 +1,3 @@ google-api-python-client==2.131.0 google-cloud-storage==2.9.0 -pytest==8.2.0 +pytest==9.0.3 diff --git a/dataflow/gpu-examples/tensorflow-minimal/requirements.txt b/dataflow/gpu-examples/tensorflow-minimal/requirements.txt index af96d7c55f5..ca6145fc750 100644 --- a/dataflow/gpu-examples/tensorflow-minimal/requirements.txt +++ b/dataflow/gpu-examples/tensorflow-minimal/requirements.txt @@ -1,2 +1,2 @@ -apache-beam[gcp]==2.46.0 -tensorflow==2.12.0 # Check TensorFlow/CUDA compatibility with Dockerfile: https://www.tensorflow.org/install/source#gpu +apache-beam[gcp]==2.74.0 +tensorflow==2.21.0 # Check TensorFlow/CUDA compatibility with Dockerfile: https://www.tensorflow.org/install/source#gpu diff --git a/dataflow/run-inference/noxfile_config.py b/dataflow/run-inference/noxfile_config.py index a2c4bb212c8..feeabc69601 100644 --- a/dataflow/run-inference/noxfile_config.py +++ b/dataflow/run-inference/noxfile_config.py @@ -22,8 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # Only test on Python 3.11. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/dataflow/run-inference/requirements-test.txt b/dataflow/run-inference/requirements-test.txt index c9095c832fd..a88ea434900 100644 --- a/dataflow/run-inference/requirements-test.txt +++ b/dataflow/run-inference/requirements-test.txt @@ -1,4 +1,4 @@ -google-cloud-aiplatform==1.57.0 +google-cloud-aiplatform==1.158.0 google-cloud-dataflow-client==0.8.14 google-cloud-storage==2.10.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/dataflow/run-inference/requirements.txt b/dataflow/run-inference/requirements.txt index 585334e1a9b..9b2b60247c7 100644 --- a/dataflow/run-inference/requirements.txt +++ b/dataflow/run-inference/requirements.txt @@ -1,3 +1,3 @@ apache-beam[gcp]==2.49.0 -torch==2.2.2 -transformers==4.38.0 +torch==2.12.1 +transformers==5.0.0rc3 diff --git a/dataflow/run-inference/tests/e2e_test.py b/dataflow/run-inference/tests/e2e_test.py index 0428af3dd28..70be7d6878d 100644 --- a/dataflow/run-inference/tests/e2e_test.py +++ b/dataflow/run-inference/tests/e2e_test.py @@ -95,7 +95,7 @@ def dataflow_job( ) -> Iterator[str]: # Upload the state dict to Cloud Storage. state_dict_gcs = f"gs://{bucket_name}/temp/state_dict.pt" - conftest.run_cmd("gsutil", "cp", "-n", state_dict_path, state_dict_gcs) + conftest.run_cmd("gcloud", "storage", "cp", "--no-clobber", state_dict_path, state_dict_gcs) # Launch the streaming Dataflow pipeline. conftest.run_cmd( diff --git a/dataflow/run_template/README.md b/dataflow/run_template/README.md index c73fb76c504..dda9f4570aa 100644 --- a/dataflow/run_template/README.md +++ b/dataflow/run_template/README.md @@ -29,7 +29,7 @@ Additionally, for this sample you need the following: ```sh export BUCKET=your-gcs-bucket - gsutil mb gs://$BUCKET + gcloud storage buckets create gs://$BUCKET ``` 1. Clone the `python-docs-samples` repository. diff --git a/dataflow/run_template/requirements-test.txt b/dataflow/run_template/requirements-test.txt index 1cb9b76028c..95e3b7c7805 100644 --- a/dataflow/run_template/requirements-test.txt +++ b/dataflow/run_template/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 -flask==3.0.3 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' backoff==2.2.1; python_version >= "3.7" diff --git a/dataflow/snippets/Dockerfile b/dataflow/snippets/Dockerfile index bb230e64e4d..4234ca52651 100644 --- a/dataflow/snippets/Dockerfile +++ b/dataflow/snippets/Dockerfile @@ -18,7 +18,7 @@ # on the host machine. This Dockerfile is derived from the # dataflow/custom-containers/ubuntu sample. -FROM python:3.12-slim +FROM python:3.14-slim # Install JRE COPY --from=openjdk:8-jre-slim /usr/local/openjdk-8 /usr/local/openjdk-8 @@ -28,7 +28,7 @@ RUN update-alternatives --install /usr/bin/java java /usr/local/openjdk-8/bin/ja WORKDIR /pipeline # Copy files from official SDK image. -COPY --from=apache/beam_python3.11_sdk:2.63.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam # Set the entrypoint to Apache Beam SDK launcher. ENTRYPOINT [ "/opt/apache/beam/boot" ] @@ -37,7 +37,7 @@ RUN apt-get update RUN apt-get install -y --no-install-recommends docker.io # Install dependencies. -RUN pip3 install --no-cache-dir apache-beam[gcp]==2.63.0 +RUN pip3 install --no-cache-dir apache-beam[gcp]==2.73.0 RUN pip install --no-cache-dir kafka-python==2.0.6 # Verify that the image does not have conflicting dependencies. diff --git a/dataflow/snippets/noxfile_config.py b/dataflow/snippets/noxfile_config.py index 900f58e0ddf..7760eb40877 100644 --- a/dataflow/snippets/noxfile_config.py +++ b/dataflow/snippets/noxfile_config.py @@ -22,6 +22,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.13"], # Old samples are opted out of enforcing Python type hints # All new samples should feature them diff --git a/dataflow/snippets/requirements-test.txt b/dataflow/snippets/requirements-test.txt index f7b11f32fc5..4835ba7a447 100644 --- a/dataflow/snippets/requirements-test.txt +++ b/dataflow/snippets/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" docker==7.1.0 diff --git a/datalabeling/snippets/noxfile_config.py b/datalabeling/snippets/noxfile_config.py index b6ac0672eb5..af568dc67ca 100644 --- a/datalabeling/snippets/noxfile_config.py +++ b/datalabeling/snippets/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"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Declare optional test sessions you want to opt-in. Currently we # have the following optional test sessions: # 'cloud_run' # Test session for Cloud Run application. diff --git a/datalabeling/snippets/requirements-test.txt b/datalabeling/snippets/requirements-test.txt index f3230681cda..79932f83530 100644 --- a/datalabeling/snippets/requirements-test.txt +++ b/datalabeling/snippets/requirements-test.txt @@ -1,2 +1,2 @@ backoff==2.2.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/dataplex/quickstart/noxfile_config.py b/dataplex/quickstart/noxfile_config.py index 457e86f5413..0973c8621c7 100644 --- a/dataplex/quickstart/noxfile_config.py +++ b/dataplex/quickstart/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/dataplex/snippets/create_data_profile_scan_global.py b/dataplex/snippets/create_data_profile_scan_global.py new file mode 100644 index 00000000000..3675521c065 --- /dev/null +++ b/dataplex/snippets/create_data_profile_scan_global.py @@ -0,0 +1,69 @@ +# 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 dataplex_create_data_profile_scan_global] +import google.api_core.exceptions +from google.cloud import dataplex_v1 + + +def create_data_profile_scan_global( + project_id: str, + dataset_id: str, + table_id: str, + location: str, +) -> None: + """Creates a Dataplex Data Profile Scan using global API endpoint routing. + + Args: + project_id (str): Google Cloud project ID where the scan is created. + dataset_id (str): Target BigQuery dataset ID. + table_id (str): Target BigQuery table ID to scan. + location (str): Google Cloud region where serverless compute runs. + """ + client = dataplex_v1.DataScanServiceClient() + + parent = client.common_location_path(project=project_id, location=location) + + bigquery_table = ( + f"//bigquery.googleapis.com/projects/{project_id}" + f"/datasets/{dataset_id}/tables/{table_id}" + ) + + data_profile_spec = dataplex_v1.DataProfileSpec(sampling_percent=100.0) + + data_scan = dataplex_v1.DataScan( + display_name="Global Data Profile Scan", + description="Regional data profile scan generating automated table statistics.", + data=dataplex_v1.DataSource(resource=bigquery_table), + data_profile_spec=data_profile_spec, + ) + + request = dataplex_v1.CreateDataScanRequest( + parent=parent, + data_scan=data_scan, + ) + + try: + operation = client.create_data_scan(request=request) + print(operation.result()) + + except google.api_core.exceptions.AlreadyExists: + print("A scan with this ID already exists.") + except google.api_core.exceptions.InvalidArgument as e: + print(f"Your scan configuration is invalid: {e}") + except google.api_core.exceptions.GoogleAPIError as e: + print(f"Unexpected exception: {e}") + + +# [END dataplex_create_data_profile_scan_global] diff --git a/dataplex/snippets/create_data_quality_scan_global.py b/dataplex/snippets/create_data_quality_scan_global.py new file mode 100644 index 00000000000..b5ef966d066 --- /dev/null +++ b/dataplex/snippets/create_data_quality_scan_global.py @@ -0,0 +1,83 @@ +# 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 dataplex_create_data_quality_scan_global] +import google.api_core.exceptions +from google.cloud import dataplex_v1 + + +def create_data_quality_scan_global( + project_id: str, + dataset_id: str, + table_id: str, + location: str, + column_id_1: str, + column_id_2: str, +) -> None: + """Creates a Dataplex Data Quality Scan using global API endpoint routing. + + Args: + project_id (str): Google Cloud project ID where the scan is created. + dataset_id (str): Target BigQuery dataset ID. + table_id (str): Target BigQuery table ID to scan. + location (str): Google Cloud region where serverless compute runs. + column_id_1 (str): Name of the first column to evaluate. + column_id_2 (str): Name of the second column to evaluate. + """ + client = dataplex_v1.DataScanServiceClient() + + parent = client.common_location_path(project=project_id, location=location) + + # A bigquery table with at least 2 columns is assumed. + bigquery_table = ( + f"//bigquery.googleapis.com/projects/{project_id}" + f"/datasets/{dataset_id}/tables/{table_id}" + ) + + data_quality_spec = dataplex_v1.DataQualitySpec( + rules=[ + dataplex_v1.DataQualityRule( + name="global-null-assertion", + dimension="COMPLETENESS", + description="Fails if any row contains a null value", + sql_assertion=dataplex_v1.DataQualityRule.SqlAssertion( + # Use ${data()} as the placeholder for the table Dataplex is scanning + sql_statement=( + "SELECT * FROM ${data()} " + f"WHERE {column_id_1} IS NULL OR {column_id_2} IS NULL" + ) + ), + ) + ] + ) + + data_scan = dataplex_v1.DataScan( + display_name="Global Data Quality Scan", + data=dataplex_v1.DataSource(resource=bigquery_table), + data_quality_spec=data_quality_spec, + ) + + request = dataplex_v1.CreateDataScanRequest(parent=parent, data_scan=data_scan) + + try: + operation = client.create_data_scan(request=request) + print(operation.result()) + except google.api_core.exceptions.AlreadyExists: + print("A scan with this ID already exists.") + except google.api_core.exceptions.InvalidArgument as e: + print(f"Your scan configuration is invalid: {e}") + except google.api_core.exceptions.GoogleAPIError as e: + print(f"Unexpected exception: {e}") + +# [END dataplex_create_data_quality_scan_global] diff --git a/dataplex/snippets/noxfile_config.py b/dataplex/snippets/noxfile_config.py index 457e86f5413..0973c8621c7 100644 --- a/dataplex/snippets/noxfile_config.py +++ b/dataplex/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/dataplex/snippets/requirements-test.txt b/dataplex/snippets/requirements-test.txt index 40543aababf..b3b2b3ca4cc 100644 --- a/dataplex/snippets/requirements-test.txt +++ b/dataplex/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.3.3 +pytest==9.0.3 diff --git a/dataplex/snippets/requirements.txt b/dataplex/snippets/requirements.txt index abaf6c843d8..07ce35f2031 100644 --- a/dataplex/snippets/requirements.txt +++ b/dataplex/snippets/requirements.txt @@ -1 +1 @@ -google-cloud-dataplex==2.4.0 +google-cloud-dataplex==2.20.0 diff --git a/dataproc/snippets/README.md b/dataproc/snippets/README.md index 98622be7dc1..442b8fa55bb 100644 --- a/dataproc/snippets/README.md +++ b/dataproc/snippets/README.md @@ -64,7 +64,7 @@ To run list_clusters.py: To run submit_job_to_cluster.py, first create a GCS bucket (used by Cloud Dataproc to stage files) from the Cloud Console or with gsutil: - gsutil mb gs:// + gcloud storage buckets create gs:// Next, set the following environment variables: diff --git a/dataproc/snippets/noxfile_config.py b/dataproc/snippets/noxfile_config.py index 99f474dc0b6..ebf6e09f415 100644 --- a/dataproc/snippets/noxfile_config.py +++ b/dataproc/snippets/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.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/dataproc/snippets/python-api-walkthrough.md b/dataproc/snippets/python-api-walkthrough.md index c5eb884a8f0..2ca94cca3ca 100644 --- a/dataproc/snippets/python-api-walkthrough.md +++ b/dataproc/snippets/python-api-walkthrough.md @@ -65,7 +65,7 @@ an explanation of how the code works. * To create a new bucket, run the following command. Your bucket name must be unique. - gsutil mb -p {{project-id}} gs://your-bucket-name + gcloud storage buckets create --project={{project-id}} gs://your-bucket-name 2. Set environment variables. @@ -145,12 +145,12 @@ Cluster cluster-name successfully deleted. If you created a Cloud Storage bucket to use for this walkthrough, you can run the following command to delete the bucket (the bucket must be empty). - gsutil rb gs://$BUCKET + gcloud storage buckets delete gs://$BUCKET * You can run the following command to **delete the bucket and all objects within it. Note: the deleted objects cannot be recovered.** - gsutil rm -r gs://$BUCKET + gcloud storage rm --recursive gs://$BUCKET * **For more information.** See the [Dataproc documentation](https://cloud.google.com/dataproc/docs/) diff --git a/dataproc/snippets/requirements-test.txt b/dataproc/snippets/requirements-test.txt index 3cb027c3ea4..8c1be52bfa9 100644 --- a/dataproc/snippets/requirements-test.txt +++ b/dataproc/snippets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-xdist==3.3.0 \ No newline at end of file diff --git a/datastore/cloud-client/requirements-test.txt b/datastore/cloud-client/requirements-test.txt index 2a635ea7b6a..322ae75b68c 100644 --- a/datastore/cloud-client/requirements-test.txt +++ b/datastore/cloud-client/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 diff --git a/datastore/cloud-ndb/noxfile_config.py b/datastore/cloud-ndb/noxfile_config.py index 25d1d4e081c..50cd5669209 100644 --- a/datastore/cloud-ndb/noxfile_config.py +++ b/datastore/cloud-ndb/noxfile_config.py @@ -25,7 +25,7 @@ # > ℹ️ Test only on Python 3.10. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.9", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/datastore/cloud-ndb/requirements-test.txt b/datastore/cloud-ndb/requirements-test.txt index 8ce117fb56e..36921e74453 100644 --- a/datastore/cloud-ndb/requirements-test.txt +++ b/datastore/cloud-ndb/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/datastore/cloud-ndb/requirements.txt b/datastore/cloud-ndb/requirements.txt index 35949d51f53..8de16307c20 100644 --- a/datastore/cloud-ndb/requirements.txt +++ b/datastore/cloud-ndb/requirements.txt @@ -1,3 +1,3 @@ google-cloud-ndb==2.3.4 -Flask==3.0.3 -Werkzeug==3.0.6 +Flask==3.1.3; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/datastore/samples/snippets/requirements-test.txt b/datastore/samples/snippets/requirements-test.txt new file mode 100644 index 00000000000..cf225d98c69 --- /dev/null +++ b/datastore/samples/snippets/requirements-test.txt @@ -0,0 +1,4 @@ +backoff===1.11.1; python_version < "3.7" +backoff==2.2.1; python_version >= "3.7" +pytest==9.0.3; 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..c9e154ba440 --- /dev/null +++ b/datastore/samples/snippets/schedule-export/requirements-test.txt @@ -0,0 +1 @@ +pytest==9.0.3; 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..601efb269d6 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": ["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-test.txt b/dialogflow-cx/requirements-test.txt index f15b2186bd1..3afe4814dde 100644 --- a/dialogflow-cx/requirements-test.txt +++ b/dialogflow-cx/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.21.1 \ No newline at end of file diff --git a/dialogflow-cx/requirements.txt b/dialogflow-cx/requirements.txt index f6c8da4cde7..0c22df90530 100644 --- a/dialogflow-cx/requirements.txt +++ b/dialogflow-cx/requirements.txt @@ -1,8 +1,8 @@ -google-cloud-dialogflow-cx==1.38.0 -Flask==3.0.3 +google-cloud-dialogflow-cx==2.0.0 +Flask==3.1.3; python_version >= '3.9' python-dateutil==2.9.0.post0 functions-framework==3.9.2 -Werkzeug==3.0.6 +Werkzeug==3.1.8; python_version >= '3.9' 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 +pyaudio==0.2.14 diff --git a/dialogflow/detect_intent_texts_with_location.py b/dialogflow/detect_intent_texts_with_location.py index d52ac178dd7..99e2eabb2f3 100644 --- a/dialogflow/detect_intent_texts_with_location.py +++ b/dialogflow/detect_intent_texts_with_location.py @@ -58,7 +58,7 @@ def detect_intent_texts_with_location( print("=" * 20) print(f"Query text: {response.query_result.query_text}") print( - f"Detected intent: {response.query_result.intent.display_name} (confidence: {response.query_result.intent_detection_confidence,})\n" + f"Detected intent: {response.query_result.intent.display_name} (confidence: {response.query_result.intent_detection_confidence})\n" ) print(f"Fulfillment text: {response.query_result.fulfillment_text}\n") diff --git a/dialogflow/noxfile_config.py b/dialogflow/noxfile_config.py index 3285f14ee14..0c031160d38 100644 --- a/dialogflow/noxfile_config.py +++ b/dialogflow/noxfile_config.py @@ -25,7 +25,7 @@ # # Disable these tests for now until there is time for significant refactoring # related to exception handling and timeouts. - "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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/dialogflow/participant_management.py b/dialogflow/participant_management.py index e2f9a486c1a..d0bfa9decf3 100644 --- a/dialogflow/participant_management.py +++ b/dialogflow/participant_management.py @@ -196,6 +196,7 @@ def analyze_content_audio_stream( timeout: int, language_code: str, single_utterance=False, + output_multiple_utterances=False, ): import google.auth from google.cloud import dialogflow_v2beta1 as dialogflow @@ -231,7 +232,9 @@ def gen_requests(participant_name, audio_config, stream): """Generates requests for streaming.""" audio_generator = stream.generator() yield dialogflow.types.participant.StreamingAnalyzeContentRequest( - participant=participant_name, audio_config=audio_config + participant=participant_name, + audio_config=audio_config, + output_multiple_utterances=output_multiple_utterances ) for content in audio_generator: yield dialogflow.types.participant.StreamingAnalyzeContentRequest( diff --git a/dialogflow/requirements-test.txt b/dialogflow/requirements-test.txt index 185d62c4204..23df1e03c7e 100644 --- a/dialogflow/requirements-test.txt +++ b/dialogflow/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 diff --git a/dialogflow/requirements.txt b/dialogflow/requirements.txt index 4c7d355eb45..38398eaee70 100644 --- a/dialogflow/requirements.txt +++ b/dialogflow/requirements.txt @@ -1,6 +1,6 @@ -google-cloud-dialogflow==2.36.0 -Flask==3.0.3 +google-cloud-dialogflow==2.46.0 +Flask==3.1.3; python_version >= '3.9' pyaudio==0.2.14 termcolor==3.0.0 functions-framework==3.9.2 -Werkzeug==3.0.6 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/dialogflow/streaming_transcription.py b/dialogflow/streaming_transcription.py index 6395a30b3a8..fe88afb581f 100644 --- a/dialogflow/streaming_transcription.py +++ b/dialogflow/streaming_transcription.py @@ -34,7 +34,7 @@ import re import sys -from google.api_core.exceptions import DeadlineExceeded +from google.api_core.exceptions import DeadlineExceeded, OutOfRange import pyaudio @@ -51,6 +51,7 @@ CHUNK_SIZE = int(SAMPLE_RATE / 10) # 100ms RESTART_TIMEOUT = 160 # seconds MAX_LOOKBACK = 3 # seconds +HALF_CLOSE_DURATION_MS = 90 * 1000 # milliseconds YELLOW = "\033[0;33m" @@ -198,6 +199,9 @@ def main(): timeout=RESTART_TIMEOUT, language_code="en-US", single_utterance=False, + # Uncomment to process multiple utterances detected in the audio stream + # individually instead of stitching together to form a single utterance. + # output_multiple_utterances=True, ) # Now, print the final transcription responses to user. @@ -213,8 +217,10 @@ def main(): offset.seconds * 1000 + offset.microseconds / 1000 ) transcript = response.recognition_result.transcript - # Half-close the stream with gRPC (in Python just stop yielding requests) - stream.is_final = True + # Half-close upon final results for better streaming experiences + # (in Python just stop yielding requests) + if stream.is_final_offset > HALF_CLOSE_DURATION_MS: + stream.is_final = True # Exit recognition if any of the transcribed phrase could be # one of our keywords. if re.search(r"\b(exit|quit)\b", transcript, re.I): @@ -223,6 +229,8 @@ def main(): terminate = True stream.closed = True break + except OutOfRange: + print("Maximum audio duration exceeded in the stream, restarting.") except DeadlineExceeded: print("Deadline Exceeded, restarting.") diff --git a/discoveryengine/answer_query_sample.py b/discoveryengine/answer_query_sample.py index 80e02e0c7c5..92d51c43a71 100644 --- a/discoveryengine/answer_query_sample.py +++ b/discoveryengine/answer_query_sample.py @@ -69,7 +69,8 @@ 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 + # Use the 2026 stable production model for answer generation + model_version="gemini-2.5-flash/answer_gen/stable", ), prompt_spec=discoveryengine.AnswerQueryRequest.AnswerGenerationSpec.PromptSpec( preamble="Give a detailed answer.", # Optional: Natural language instructions for customizing the answer. diff --git a/discoveryengine/requirements-test.txt b/discoveryengine/requirements-test.txt index e8a7ae0cb55..8dbf80c54bc 100644 --- a/discoveryengine/requirements-test.txt +++ b/discoveryengine/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-api-core==2.21.0 google-cloud-resource-manager==1.12.5 diff --git a/discoveryengine/standalone_apis_sample.py b/discoveryengine/standalone_apis_sample.py index b39794d4bc5..1a0ff112904 100644 --- a/discoveryengine/standalone_apis_sample.py +++ b/discoveryengine/standalone_apis_sample.py @@ -143,7 +143,7 @@ def grounded_generation_inline_vais_sample( # Format: projects/{project_number}/locations/{location} location=client.common_location_path(project=project_number, location="global"), generation_spec=discoveryengine.GenerateGroundedContentRequest.GenerationSpec( - model_id="default", + model_id="gemini-2.5-flash", ), # Conversation between user and model contents=[ @@ -215,7 +215,7 @@ def grounded_generation_google_search_sample( # Format: projects/{project_number}/locations/{location} location=client.common_location_path(project=project_number, location="global"), generation_spec=discoveryengine.GenerateGroundedContentRequest.GenerationSpec( - model_id="default", + model_id="gemini-2.5-flash", ), # Conversation between user and model contents=[ @@ -274,7 +274,7 @@ def grounded_generation_streaming_sample( # Format: projects/{project_number}/locations/{location} location=client.common_location_path(project=project_number, location="global"), generation_spec=discoveryengine.GenerateGroundedContentRequest.GenerationSpec( - model_id="default", + model_id="gemini-2.5-flash", ), # Conversation between user and model contents=[ diff --git a/dlp/snippets/noxfile_config.py b/dlp/snippets/noxfile_config.py index 798647726bd..295ef23e354 100644 --- a/dlp/snippets/noxfile_config.py +++ b/dlp/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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/dlp/snippets/requirements-test.txt b/dlp/snippets/requirements-test.txt index 632e5ad15a8..0d2bade1bed 100644 --- a/dlp/snippets/requirements-test.txt +++ b/dlp/snippets/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 diff --git a/documentai/snippets/noxfile_config.py b/documentai/snippets/noxfile_config.py index 24b01d2b9ab..0973c8621c7 100644 --- a/documentai/snippets/noxfile_config.py +++ b/documentai/snippets/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.6", "3.8", "3.9", "3.10", "3.11"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/documentai/snippets/requirements-test.txt b/documentai/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/documentai/snippets/requirements-test.txt +++ b/documentai/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/endpoints/bookstore-grpc-transcoding/requirements-test.txt b/endpoints/bookstore-grpc-transcoding/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/endpoints/bookstore-grpc-transcoding/requirements-test.txt +++ b/endpoints/bookstore-grpc-transcoding/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/endpoints/bookstore-grpc/requirements-test.txt b/endpoints/bookstore-grpc/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/endpoints/bookstore-grpc/requirements-test.txt +++ b/endpoints/bookstore-grpc/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/endpoints/getting-started-grpc/requirements-test.txt b/endpoints/getting-started-grpc/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/endpoints/getting-started-grpc/requirements-test.txt +++ b/endpoints/getting-started-grpc/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/endpoints/getting-started/clients/service_to_service_non_default/requirements-test.txt b/endpoints/getting-started/clients/service_to_service_non_default/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/endpoints/getting-started/clients/service_to_service_non_default/requirements-test.txt +++ b/endpoints/getting-started/clients/service_to_service_non_default/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/endpoints/getting-started/noxfile_config.py b/endpoints/getting-started/noxfile_config.py index 26f09f74ce6..50cd5669209 100644 --- a/endpoints/getting-started/noxfile_config.py +++ b/endpoints/getting-started/noxfile_config.py @@ -25,7 +25,7 @@ # > ℹ️ Test only on Python 3.10. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/endpoints/getting-started/requirements-test.txt b/endpoints/getting-started/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/endpoints/getting-started/requirements-test.txt +++ b/endpoints/getting-started/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/endpoints/getting-started/requirements.txt b/endpoints/getting-started/requirements.txt index ea1c7021fd5..dcc5cdd487b 100644 --- a/endpoints/getting-started/requirements.txt +++ b/endpoints/getting-started/requirements.txt @@ -1,9 +1,9 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' flask-cors==6.0.1 gunicorn==23.0.0 six==1.16.0 pyyaml==6.0.2 -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' google-auth==2.38.0 google-auth-oauthlib==1.2.1 -Werkzeug==3.0.6 \ No newline at end of file +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/enterpriseknowledgegraph/entity_reconciliation/requirements-test.txt b/enterpriseknowledgegraph/entity_reconciliation/requirements-test.txt index 55d9a1d34d9..2313637c624 100644 --- a/enterpriseknowledgegraph/entity_reconciliation/requirements-test.txt +++ b/enterpriseknowledgegraph/entity_reconciliation/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-api-core google-cloud-enterpriseknowledgegraph diff --git a/enterpriseknowledgegraph/search/requirements-test.txt b/enterpriseknowledgegraph/search/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/enterpriseknowledgegraph/search/requirements-test.txt +++ b/enterpriseknowledgegraph/search/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/error_reporting/fluent_on_compute/requirements-test.txt b/error_reporting/fluent_on_compute/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/error_reporting/fluent_on_compute/requirements-test.txt +++ b/error_reporting/fluent_on_compute/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/error_reporting/snippets/requirements-test.txt b/error_reporting/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/error_reporting/snippets/requirements-test.txt +++ b/error_reporting/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/eventarc/audit-storage/Dockerfile b/eventarc/audit-storage/Dockerfile index d5b1fad1c17..e65ba1afa4d 100644 --- a/eventarc/audit-storage/Dockerfile +++ b/eventarc/audit-storage/Dockerfile @@ -16,7 +16,7 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.11-slim +FROM python:3.14-slim # Allow statements and log messages to immediately appear in the Cloud Run logs ENV PYTHONUNBUFFERED True diff --git a/eventarc/audit-storage/requirements-test.txt b/eventarc/audit-storage/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/eventarc/audit-storage/requirements-test.txt +++ b/eventarc/audit-storage/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/eventarc/audit-storage/requirements.txt b/eventarc/audit-storage/requirements.txt index fcbf5097b8c..62b5bdfe9a3 100644 --- a/eventarc/audit-storage/requirements.txt +++ b/eventarc/audit-storage/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 cloudevents==1.11.0 diff --git a/eventarc/audit_iam/requirements-test.txt b/eventarc/audit_iam/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/eventarc/audit_iam/requirements-test.txt +++ b/eventarc/audit_iam/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/eventarc/audit_iam/requirements.txt b/eventarc/audit_iam/requirements.txt index 11317349789..5cf94554eaa 100644 --- a/eventarc/audit_iam/requirements.txt +++ b/eventarc/audit_iam/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 google-events==0.14.0 cloudevents==1.11.0 diff --git a/eventarc/generic/Dockerfile b/eventarc/generic/Dockerfile index a7a158bcb39..e4f1889dc5b 100644 --- a/eventarc/generic/Dockerfile +++ b/eventarc/generic/Dockerfile @@ -14,7 +14,7 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.11-slim +FROM python:3.14-slim # Allow statements and log messages to immediately appear in the Cloud Run logs ENV PYTHONUNBUFFERED True diff --git a/eventarc/generic/requirements-test.txt b/eventarc/generic/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/eventarc/generic/requirements-test.txt +++ b/eventarc/generic/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/eventarc/generic/requirements.txt b/eventarc/generic/requirements.txt index 9ea9c8a9310..6b693f8f926 100644 --- a/eventarc/generic/requirements.txt +++ b/eventarc/generic/requirements.txt @@ -1,2 +1,2 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 diff --git a/eventarc/pubsub/Dockerfile b/eventarc/pubsub/Dockerfile index a7a158bcb39..e4f1889dc5b 100644 --- a/eventarc/pubsub/Dockerfile +++ b/eventarc/pubsub/Dockerfile @@ -14,7 +14,7 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.11-slim +FROM python:3.14-slim # Allow statements and log messages to immediately appear in the Cloud Run logs ENV PYTHONUNBUFFERED True diff --git a/eventarc/pubsub/requirements-test.txt b/eventarc/pubsub/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/eventarc/pubsub/requirements-test.txt +++ b/eventarc/pubsub/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/eventarc/pubsub/requirements.txt b/eventarc/pubsub/requirements.txt index 9ea9c8a9310..6b693f8f926 100644 --- a/eventarc/pubsub/requirements.txt +++ b/eventarc/pubsub/requirements.txt @@ -1,2 +1,2 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 diff --git a/eventarc/storage_handler/requirements-test.txt b/eventarc/storage_handler/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/eventarc/storage_handler/requirements-test.txt +++ b/eventarc/storage_handler/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/eventarc/storage_handler/requirements.txt b/eventarc/storage_handler/requirements.txt index f54b23da042..dced49189df 100644 --- a/eventarc/storage_handler/requirements.txt +++ b/eventarc/storage_handler/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 google-events==0.14.0 cloudevents==1.11.0 diff --git a/firestore/cloud-async-client/requirements-test.txt b/firestore/cloud-async-client/requirements-test.txt index 8e8f4270ac6..e8081946e32 100644 --- a/firestore/cloud-async-client/requirements-test.txt +++ b/firestore/cloud-async-client/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.21.0; python_version > '3.6' pytest-asyncio==0.16.0; python_version < '3.7' flaky==3.8.1 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/requirements-test.txt b/firestore/cloud-client/requirements-test.txt index 632e5ad15a8..0d2bade1bed 100644 --- a/firestore/cloud-client/requirements-test.txt +++ b/firestore/cloud-client/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 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/functions/bigtable/requirements-test.txt b/functions/bigtable/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/bigtable/requirements-test.txt +++ b/functions/bigtable/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/billing/requirements-test.txt b/functions/billing/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/billing/requirements-test.txt +++ b/functions/billing/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/billing_stop_on_notification/requirements-test.txt b/functions/billing_stop_on_notification/requirements-test.txt index 66801836e20..34976cd306a 100644 --- a/functions/billing_stop_on_notification/requirements-test.txt +++ b/functions/billing_stop_on_notification/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.3.5 +pytest==9.0.3; python_version >= "3.10" cloudevents==1.11.0 \ No newline at end of file diff --git a/functions/billing_stop_on_notification/requirements.txt b/functions/billing_stop_on_notification/requirements.txt index 912b07cd0a3..b730a52aa07 100644 --- a/functions/billing_stop_on_notification/requirements.txt +++ b/functions/billing_stop_on_notification/requirements.txt @@ -1,3 +1,5 @@ +# [START functions_billing_stop_requirements] functions-framework==3.* google-cloud-billing==1.16.2 google-cloud-logging==3.12.1 +# [END functions_billing_stop_requirements] diff --git a/functions/concepts-filesystem/requirements-test.txt b/functions/concepts-filesystem/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/concepts-filesystem/requirements-test.txt +++ b/functions/concepts-filesystem/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/concepts-requests/requirements-test.txt b/functions/concepts-requests/requirements-test.txt index 8122137827f..cf909bc887e 100644 --- a/functions/concepts-requests/requirements-test.txt +++ b/functions/concepts-requests/requirements-test.txt @@ -1,5 +1,5 @@ -flask==3.0.3 -pytest==8.2.0 -requests==2.31.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' responses==0.17.0; python_version < '3.7' responses==0.23.1; python_version > '3.6' diff --git a/functions/concepts-requests/requirements.txt b/functions/concepts-requests/requirements.txt index e8dc91f5eb5..a06420244a9 100644 --- a/functions/concepts-requests/requirements.txt +++ b/functions/concepts-requests/requirements.txt @@ -1,2 +1,2 @@ functions-framework==3.9.2 -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/functions/concepts-stateless/requirements-test.txt b/functions/concepts-stateless/requirements-test.txt index 06c13ca892f..6be17631c48 100644 --- a/functions/concepts-stateless/requirements-test.txt +++ b/functions/concepts-stateless/requirements-test.txt @@ -1,3 +1,3 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 diff --git a/functions/env_vars/requirements-test.txt b/functions/env_vars/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/env_vars/requirements-test.txt +++ b/functions/env_vars/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/firebase/requirements-test.txt b/functions/firebase/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/firebase/requirements-test.txt +++ b/functions/firebase/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/helloworld/requirements-test.txt b/functions/helloworld/requirements-test.txt index 6031c4d8ee4..dcffe86697c 100644 --- a/functions/helloworld/requirements-test.txt +++ b/functions/helloworld/requirements-test.txt @@ -1,3 +1,3 @@ functions-framework==3.9.2 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" uuid==1.30 diff --git a/functions/helloworld/requirements.txt b/functions/helloworld/requirements.txt index 8c9cb7ea6d4..11e15adca3e 100644 --- a/functions/helloworld/requirements.txt +++ b/functions/helloworld/requirements.txt @@ -1,4 +1,4 @@ functions-framework==3.9.2 -flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' google-cloud-error-reporting==1.11.1 MarkupSafe==2.1.3 diff --git a/functions/http/requirements-test.txt b/functions/http/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/http/requirements-test.txt +++ b/functions/http/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/imagemagick/README.md b/functions/imagemagick/README.md index 8b75b781c40..67de45c5731 100644 --- a/functions/imagemagick/README.md +++ b/functions/imagemagick/README.md @@ -21,13 +21,13 @@ Functions for your project. 1. Create a Cloud Storage Bucket: - gsutil mb gs://YOUR_INPUT_BUCKET_NAME + gcloud storage buckets create gs://YOUR_INPUT_BUCKET_NAME This storage bucket is used to upload images for the function to check. 1. Create a second Cloud Storage Bucket: - gsutil mb gs://YOUR_OUTPUT_BUCKET_NAME + gcloud storage buckets create gs://YOUR_OUTPUT_BUCKET_NAME This second storage bucket is used to store blurred images. (Un-blurred images will not be saved to this bucket.) diff --git a/functions/imagemagick/requirements-dev.txt b/functions/imagemagick/requirements-dev.txt index 158a3587da8..3bbf38755d9 100644 --- a/functions/imagemagick/requirements-dev.txt +++ b/functions/imagemagick/requirements-dev.txt @@ -1,4 +1,4 @@ uuid==1.30 -pytest==8.2.0; python_version > "3.0" +pytest==9.0.3; python_version > "3.0" # pin pytest to 4.6.11 or lower for Python2. -pytest==4.6.11; python_version < "3.0" +pytest==9.0.3; python_version < "3.0" diff --git a/functions/imagemagick/requirements-test.txt b/functions/imagemagick/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/imagemagick/requirements-test.txt +++ b/functions/imagemagick/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/log/requirements-test.txt b/functions/log/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/log/requirements-test.txt +++ b/functions/log/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/memorystore/redis/requirements-test.txt b/functions/memorystore/redis/requirements-test.txt index f110b22346d..8805fc0af69 100644 --- a/functions/memorystore/redis/requirements-test.txt +++ b/functions/memorystore/redis/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/ocr/app/main.py b/functions/ocr/app/main.py index 186c9abfaaa..53bf3467d1d 100644 --- a/functions/ocr/app/main.py +++ b/functions/ocr/app/main.py @@ -69,7 +69,7 @@ def detect_text(bucket: str, filename: str) -> None: filename: name of the file to be read. Returns: - None; the output is written to stdout and Stackdriver Logging. + None; the output is written to stdout and Cloud Logging. """ print("Looking for text in image {}".format(filename)) @@ -123,7 +123,7 @@ def process_image(file_info: dict, context: dict) -> None: context: a dictionary containing metadata about the event. Returns: - None; the output is written to stdout and Stackdriver Logging. + None; the output is written to stdout and Cloud Logging. """ bucket = validate_message(file_info, "bucket") name = validate_message(file_info, "name") @@ -148,7 +148,7 @@ def translate_text(event: dict, context: dict) -> None: context: a dictionary containing metadata about the event. Returns: - None; the output is written to stdout and Stackdriver Logging. + None; the output is written to stdout and Cloud Logging. """ if event.get("data"): message_data = base64.b64decode(event["data"]).decode("utf-8") @@ -189,7 +189,7 @@ def save_result(event: dict, context: dict) -> None: context: a dictionary containing metadata about the event. Returns: - None; the output is written to stdout and Stackdriver Logging. + None; the output is written to stdout and Cloud Logging. """ if event.get("data"): message_data = base64.b64decode(event["data"]).decode("utf-8") diff --git a/functions/ocr/app/noxfile_config.py b/functions/ocr/app/noxfile_config.py index de1a75b6996..ba2433bfcbd 100644 --- a/functions/ocr/app/noxfile_config.py +++ b/functions/ocr/app/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"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Declare optional test sessions you want to opt-in. Currently we # have the following optional test sessions: # 'cloud_run' # Test session for Cloud Run application. diff --git a/functions/ocr/app/requirements-test.txt b/functions/ocr/app/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/ocr/app/requirements-test.txt +++ b/functions/ocr/app/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/pubsub/requirements-test.txt b/functions/pubsub/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/pubsub/requirements-test.txt +++ b/functions/pubsub/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/security/requirements-test.txt b/functions/security/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/security/requirements-test.txt +++ b/functions/security/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/security/requirements.txt b/functions/security/requirements.txt index 2c24336eb31..5196643fbdb 100644 --- a/functions/security/requirements.txt +++ b/functions/security/requirements.txt @@ -1 +1 @@ -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/functions/slack/requirements-test.txt b/functions/slack/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/slack/requirements-test.txt +++ b/functions/slack/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/slack/requirements.txt b/functions/slack/requirements.txt index a6d5d05bb78..049295aa4b9 100644 --- a/functions/slack/requirements.txt +++ b/functions/slack/requirements.txt @@ -1,4 +1,4 @@ google-api-python-client==2.131.0 -flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 slackclient==2.9.4 diff --git a/functions/spanner/requirements-test.txt b/functions/spanner/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/spanner/requirements-test.txt +++ b/functions/spanner/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/tips-avoid-infinite-retries/requirements-test.txt b/functions/tips-avoid-infinite-retries/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/tips-avoid-infinite-retries/requirements-test.txt +++ b/functions/tips-avoid-infinite-retries/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/tips-connection-pooling/requirements-test.txt b/functions/tips-connection-pooling/requirements-test.txt index 8122137827f..cf909bc887e 100644 --- a/functions/tips-connection-pooling/requirements-test.txt +++ b/functions/tips-connection-pooling/requirements-test.txt @@ -1,5 +1,5 @@ -flask==3.0.3 -pytest==8.2.0 -requests==2.31.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' responses==0.17.0; python_version < '3.7' responses==0.23.1; python_version > '3.6' diff --git a/functions/tips-connection-pooling/requirements.txt b/functions/tips-connection-pooling/requirements.txt index a267b387ca6..2105bf05e46 100644 --- a/functions/tips-connection-pooling/requirements.txt +++ b/functions/tips-connection-pooling/requirements.txt @@ -1,2 +1,2 @@ -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' functions-framework==3.9.2 diff --git a/functions/tips-gcp-apis/requirements-test.txt b/functions/tips-gcp-apis/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/tips-gcp-apis/requirements-test.txt +++ b/functions/tips-gcp-apis/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/tips-lazy-globals/requirements-test.txt b/functions/tips-lazy-globals/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/tips-lazy-globals/requirements-test.txt +++ b/functions/tips-lazy-globals/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/tips-retry/main.py b/functions/tips-retry/main.py index 847ae6394dc..dfe11942af2 100644 --- a/functions/tips-retry/main.py +++ b/functions/tips-retry/main.py @@ -27,7 +27,7 @@ def retry_or_not(data, context): data (dict): The event payload. context (google.cloud.functions.Context): The event metadata. Returns: - None; output is written to Stackdriver Logging + None; output is written to Cloud Logging """ # Retry based on a user-defined parameter diff --git a/functions/tips-retry/requirements-test.txt b/functions/tips-retry/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/tips-retry/requirements-test.txt +++ b/functions/tips-retry/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/tips-scopes/requirements-test.txt b/functions/tips-scopes/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/tips-scopes/requirements-test.txt +++ b/functions/tips-scopes/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/v2/audit_log/requirements-test.txt b/functions/v2/audit_log/requirements-test.txt index 6e4d1d97ce0..34976cd306a 100644 --- a/functions/v2/audit_log/requirements-test.txt +++ b/functions/v2/audit_log/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" cloudevents==1.11.0 \ No newline at end of file diff --git a/functions/v2/datastore/hello-datastore/requirements-test.txt b/functions/v2/datastore/hello-datastore/requirements-test.txt index 15d066af319..d9ad1f716cc 100644 --- a/functions/v2/datastore/hello-datastore/requirements-test.txt +++ b/functions/v2/datastore/hello-datastore/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3 \ No newline at end of file diff --git a/functions/v2/datastore/hello-datastore/requirements.txt b/functions/v2/datastore/hello-datastore/requirements.txt index 35e86dbfbc5..95b25fd75f5 100644 --- a/functions/v2/datastore/hello-datastore/requirements.txt +++ b/functions/v2/datastore/hello-datastore/requirements.txt @@ -1,6 +1,6 @@ -functions-framework==3.9.2 +protobuf==6.33.6 +google-api-core==2.31.0 +functions-framework==3.10.2 google-events==0.14.0 -google-cloud-datastore==2.20.2 -google-api-core==2.17.1 -protobuf==4.25.8 -cloudevents==1.11.0 +google-cloud-datastore==2.25.0 +cloudevents==1.12.0 diff --git a/functions/v2/deploy-function/requirements-test.txt b/functions/v2/deploy-function/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/v2/deploy-function/requirements-test.txt +++ b/functions/v2/deploy-function/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/firebase/hello-firestore/requirements-test.txt b/functions/v2/firebase/hello-firestore/requirements-test.txt index 060ed652e0b..b3b2b3ca4cc 100644 --- a/functions/v2/firebase/hello-firestore/requirements-test.txt +++ b/functions/v2/firebase/hello-firestore/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3 diff --git a/functions/v2/firebase/hello-firestore/requirements.txt b/functions/v2/firebase/hello-firestore/requirements.txt index b2d03f648de..7321e055380 100644 --- a/functions/v2/firebase/hello-firestore/requirements.txt +++ b/functions/v2/firebase/hello-firestore/requirements.txt @@ -1,5 +1,5 @@ -functions-framework==3.9.2 +protobuf==6.33.6 +functions-framework==3.10.2 google-events==0.14.0 -google-api-core==2.17.1 -protobuf==4.25.6 -cloudevents==1.11.0 \ No newline at end of file +google-api-core==2.31.0 +cloudevents==1.12.0 \ No newline at end of file diff --git a/functions/v2/firebase/hello-remote-config/requirements-test.txt b/functions/v2/firebase/hello-remote-config/requirements-test.txt index 060ed652e0b..c9e154ba440 100644 --- a/functions/v2/firebase/hello-remote-config/requirements-test.txt +++ b/functions/v2/firebase/hello-remote-config/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/firebase/hello-rtdb/requirements-test.txt b/functions/v2/firebase/hello-rtdb/requirements-test.txt index 060ed652e0b..c9e154ba440 100644 --- a/functions/v2/firebase/hello-rtdb/requirements-test.txt +++ b/functions/v2/firebase/hello-rtdb/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/firebase/upper-firestore/requirements-test.txt b/functions/v2/firebase/upper-firestore/requirements-test.txt index 15d066af319..b3b2b3ca4cc 100644 --- a/functions/v2/firebase/upper-firestore/requirements-test.txt +++ b/functions/v2/firebase/upper-firestore/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3 diff --git a/functions/v2/firebase/upper-firestore/requirements.txt b/functions/v2/firebase/upper-firestore/requirements.txt index cc5c66225f4..6058a97f7e1 100644 --- a/functions/v2/firebase/upper-firestore/requirements.txt +++ b/functions/v2/firebase/upper-firestore/requirements.txt @@ -1,6 +1,6 @@ -functions-framework==3.9.2 +protobuf==6.33.6 +cloudevents==1.12.0 +functions-framework==3.10.2 google-events==0.14.0 -google-api-core==2.17.1 -protobuf==4.25.6 -google-cloud-firestore==2.19.0 -cloudevents==1.11.0 \ No newline at end of file +google-api-core==2.31.0 +google-cloud-firestore==2.27.0 diff --git a/functions/v2/http_logging/requirements-test.txt b/functions/v2/http_logging/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/v2/http_logging/requirements-test.txt +++ b/functions/v2/http_logging/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/v2/imagemagick/README.md b/functions/v2/imagemagick/README.md index f388338ea17..d4035146d33 100644 --- a/functions/v2/imagemagick/README.md +++ b/functions/v2/imagemagick/README.md @@ -21,13 +21,13 @@ Functions for your project. 1. Create a Cloud Storage Bucket: - gsutil mb gs://YOUR_INPUT_BUCKET_NAME + gcloud storage buckets create gs://YOUR_INPUT_BUCKET_NAME This storage bucket is used to upload images for the function to check. 1. Create a second Cloud Storage Bucket: - gsutil mb gs://YOUR_OUTPUT_BUCKET_NAME + gcloud storage buckets create gs://YOUR_OUTPUT_BUCKET_NAME This second storage bucket is used to store blurred images. (Images that are **not** blurred will not be saved to this bucket.) diff --git a/functions/v2/imagemagick/requirements-test.txt b/functions/v2/imagemagick/requirements-test.txt index c6ff41faf3f..b0e8a4a8ace 100644 --- a/functions/v2/imagemagick/requirements-test.txt +++ b/functions/v2/imagemagick/requirements-test.txt @@ -1,3 +1,3 @@ six==1.16.0 uuid==1.30 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/label_gce_instance/requirements-test.txt b/functions/v2/label_gce_instance/requirements-test.txt index 241f2ead47a..fb125a765d8 100644 --- a/functions/v2/label_gce_instance/requirements-test.txt +++ b/functions/v2/label_gce_instance/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" cloudevents==1.11.0 diff --git a/functions/v2/log/helloworld/requirements-test.txt b/functions/v2/log/helloworld/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/v2/log/helloworld/requirements-test.txt +++ b/functions/v2/log/helloworld/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/log/stackdriver/requirements-test.txt b/functions/v2/log/stackdriver/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/v2/log/stackdriver/requirements-test.txt +++ b/functions/v2/log/stackdriver/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/ocr/noxfile_config.py b/functions/v2/ocr/noxfile_config.py index c5762624034..2c0d1f22101 100644 --- a/functions/v2/ocr/noxfile_config.py +++ b/functions/v2/ocr/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"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Declare optional test sessions you want to opt-in. Currently we # have the following optional test sessions: # 'cloud_run' # Test session for Cloud Run application. diff --git a/functions/v2/ocr/requirements-test.txt b/functions/v2/ocr/requirements-test.txt index 8c11dec956d..70ea7e526e4 100644 --- a/functions/v2/ocr/requirements-test.txt +++ b/functions/v2/ocr/requirements-test.txt @@ -1,2 +1,2 @@ cloudevents==1.11.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/pubsub/requirements-test.txt b/functions/v2/pubsub/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/functions/v2/pubsub/requirements-test.txt +++ b/functions/v2/pubsub/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/functions/v2/response_streaming/requirements.txt b/functions/v2/response_streaming/requirements.txt index 56da3662b54..616b4166dcb 100644 --- a/functions/v2/response_streaming/requirements.txt +++ b/functions/v2/response_streaming/requirements.txt @@ -1,5 +1,5 @@ -Flask==2.2.2 +Flask==3.1.3; python_version >= '3.9' functions-framework==3.9.2 google-cloud-bigquery==3.27.0 -pytest==8.2.0 -Werkzeug==2.3.8 +pytest==9.0.3; python_version >= "3.10" +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/functions/v2/storage/noxfile_config.py b/functions/v2/storage/noxfile_config.py index 457e86f5413..0973c8621c7 100644 --- a/functions/v2/storage/noxfile_config.py +++ b/functions/v2/storage/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/functions/v2/storage/requirements-test.txt b/functions/v2/storage/requirements-test.txt index 6e4d1d97ce0..34976cd306a 100644 --- a/functions/v2/storage/requirements-test.txt +++ b/functions/v2/storage/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" cloudevents==1.11.0 \ No newline at end of file diff --git a/functions/v2/tips-avoid-infinite-retries/main.py b/functions/v2/tips-avoid-infinite-retries/main.py index e3b735f2f74..9e751d5d0b5 100644 --- a/functions/v2/tips-avoid-infinite-retries/main.py +++ b/functions/v2/tips-avoid-infinite-retries/main.py @@ -29,7 +29,7 @@ def avoid_infinite_retries(cloud_event): Args: cloud_event: The cloud event associated with the current trigger Returns: - None; output is written to Stackdriver Logging + None; output is written to Cloud Logging """ timestamp = cloud_event["time"] diff --git a/functions/v2/tips-avoid-infinite-retries/requirements-test.txt b/functions/v2/tips-avoid-infinite-retries/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/v2/tips-avoid-infinite-retries/requirements-test.txt +++ b/functions/v2/tips-avoid-infinite-retries/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/v2/tips-retry/main.py b/functions/v2/tips-retry/main.py index 7ff58a954ff..91ce20b8468 100644 --- a/functions/v2/tips-retry/main.py +++ b/functions/v2/tips-retry/main.py @@ -31,7 +31,7 @@ def retry_or_not(cloud_event): Args: cloud_event: The cloud event with a Pub/Sub data payload Returns: - None; output is written to Stackdriver Logging + None; output is written to Cloud Logging """ # The Pub/Sub event payload is passed as the CloudEvent's data payload. diff --git a/functions/v2/tips-retry/requirements-test.txt b/functions/v2/tips-retry/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/v2/tips-retry/requirements-test.txt +++ b/functions/v2/tips-retry/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/v2/typed/googlechatbot/requirements-test.txt b/functions/v2/typed/googlechatbot/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/v2/typed/googlechatbot/requirements-test.txt +++ b/functions/v2/typed/googlechatbot/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/functions/v2/typed/greeting/requirements-test.txt b/functions/v2/typed/greeting/requirements-test.txt index ef95de28389..8805fc0af69 100644 --- a/functions/v2/typed/greeting/requirements-test.txt +++ b/functions/v2/typed/greeting/requirements-test.txt @@ -1,2 +1,2 @@ -flask==3.0.3 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" +Flask==3.1.3; python_version >= '3.9' diff --git a/geminidataanalytics/dataengineeringagent/README.md b/geminidataanalytics/dataengineeringagent/README.md new file mode 100644 index 00000000000..9f18c17fc4a --- /dev/null +++ b/geminidataanalytics/dataengineeringagent/README.md @@ -0,0 +1,140 @@ +# Data Engineering Agent A2A Client Example + +This directory contains a sample Python implementation of an +[Agent-to-Agent (A2A)](https://a2a-protocol.org/) client designed to interact +with the **Google Cloud Data Engineering Agent (DEA)**. + +## Background + +The Data Engineering Agent is a BigQuery and Dataform ELT expert capable of +building, managing, and troubleshooting data pipelines. To enable +interoperability across different platforms and agents, it exposes an interface +following the A2A protocol. + +Official Documentation: +[Data Engineering Agent API Overview](https://docs.cloud.google.com/gemini/data-agents/data-engineering-agent/api-overview) + +This example demonstrates how to use the open-source +[A2A Python SDK](https://github.com/a2aproject/a2a-python) to: + +1. **Discover** the agent's capabilities via its Agent Card. +2. **Authenticate** using Google Application Default Credentials (ADC). +3. **Maintain State** across multi-turn conversations using Conversation Tokens persisted to a local file. +4. **Handle Complex Tasks** by automatically resuming execution when the agent finishes with `DEADLINE_EXCEEDED`. +5. **Configure Extensions** like `Instruction` to customize agent behavior. + +## Features + +- **Native A2A SDK Usage:** Uses `A2ACardResolver` and `create_client` for + idiomatic protocol interaction. +- **Streaming-only Execution:** Hardcoded to use response streaming for lowest + real-time latency and optimal interaction patterns. +- **Session Persistence:** Automatically saves and loads the + `conversationToken` from a local file, allowing multi-turn conversations via + repeated script executions. +- **Configurable Parameters:** Exposes `gcp_resource_id` for flexibility, + automatically extracting project and location details. +- **Instruction Loading:** Automatically reads custom instructions from local + files or directories, using filenames as the instruction name and file + content as the definition. +- **Extension Header Support:** Uses `ServiceParametersFactory` to correctly + set the `A2A-Extensions` HTTP header required by the agent. +- **Automated Resumption:** Detects `DEADLINE_EXCEEDED` via the + `finish_reason` extension and transparently continues the task. + +## Prerequisites + +- Python 3.10 or higher. +- [Google Cloud SDK (gcloud)](https://cloud.google.com/sdk/docs/install) + installed and configured. +- Enable required APIs + [Use the Data Engineering Agent to build and modify data pipelines](https://docs.cloud.google.com/bigquery/docs/data-engineering-agent-pipelines#required-apis) + +## Setup + +1. **Create and activate a virtual environment:** + + ```bash + python3 -m venv .dea + source .dea/bin/activate + ``` + +2. **Install dependencies:** + + ```bash + pip install -r requirements.txt + ``` + +3. **Authenticate with Google Cloud:** + + ```bash + gcloud auth application-default login + ``` + +## Usage + +### Single Message Mode + +Sends a single message and exits. `gcp_resource_id` and `message` are required. + +``` +python3 dea_a2a_client.py \ + --gcp_resource_id projects/my-project/locations/us-central1/repositories/my-repo/workspaces/default \ + --message "List my Dataform tables" +``` + +### Multi-turn Conversation (State Persistence) + +To maintain a conversation across multiple calls, use the +`--conversation_token_path` argument. The script will save the conversation +token to this file and reload it in subsequent calls. + +``` +# First turn (starts session) +python3 dea_a2a_client.py \ + --gcp_resource_id projects/my-project/locations/us-central1/repositories/my-repo/workspaces/default \ + --message "hi" \ + --conversation_token_path ./token.txt + +# Second turn (continues previous context) +python3 dea_a2a_client.py \ + --gcp_resource_id projects/my-project/locations/us-central1/repositories/my-repo/workspaces/default \ + --message "Explain the first table" \ + --conversation_token_path ./token.txt +``` + +### Advanced: Providing Local Instructions + +You can point the client to local files (e.g., SQL style guides) to influence +the agent's behavior. + +``` +python3 dea_a2a_client.py \ + --gcp_resource_id projects/my-project/locations/us-central1/repositories/my-repo/workspaces/default \ + --message "List my tables" \ + --instruction_path ./style_guide.md +``` + +#### Command-line Arguments + +Argument | Required | Description +:-------------------------- | :------- | :---------- +`--gcp_resource_id` | **Yes** | The target Google Cloud resource ID. Supported formats `projects/{p}/locations/{l}/repositories/{r}/workspaces/{w}` (Dataform) +`--message` | **Yes** | The message to send to the agent. +`--conversation_token_path` | No | Path to a local file to persist conversation token. Allows for multi-turn conversations. +`--instruction_path` | No | Path to a file or directory containing instructions. Can be repeated. + +### Running Unit Tests + +Make sure the virtual environment is activated, then run: + +```bash +python3 dea_a2a_client_test.py +``` + +Alternatively, if the virtual environment is not activated, you can run it +directly using the venv python: + +```bash +./.dea/bin/python3 dea_a2a_client_test.py +``` diff --git a/geminidataanalytics/dataengineeringagent/dea_a2a_client.py b/geminidataanalytics/dataengineeringagent/dea_a2a_client.py new file mode 100644 index 00000000000..117aa78dd93 --- /dev/null +++ b/geminidataanalytics/dataengineeringagent/dea_a2a_client.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python + +# 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. + +"""Example A2A client for the Data Engineering Agent. + +This script demonstrates how to interact with the Data Engineering Agent (DEA) +using the A2A Python SDK. It handles Agent Card resolution, multi-turn +conversations using conversation tokens persisted to a local file, and +automatic retries on deadline exceeded. + +The script is compatible with DEA v0.3 protocol flavor. +""" + +from __future__ import annotations + +import argparse +import asyncio +import os +import pathlib +import re +from typing import Any +import uuid + +from a2a import client as a2a_client +from a2a import types as a2a_types +from a2a.client import service_parameters as a2a_service_params +from a2a.utils import constants as a2a_constants +from google.auth import default +from google.auth.transport import requests as auth_requests +from google.protobuf import json_format as pb_json_format +import httpx + +import logging + + +# --- Constants --- + +DEA_EXT_PREFIX = "https://geminidataanalytics.googleapis.com/a2a/extensions" +GCP_RESOURCE_EXT_URI = f"{DEA_EXT_PREFIX}/gcpresource/v1" +CONVERSATION_TOKEN_EXT_URI = f"{DEA_EXT_PREFIX}/conversationtoken/v1" +FINISH_REASON_EXT_URI = f"{DEA_EXT_PREFIX}/finishreason/v1" +INSTRUCTION_EXT_URI = f"{DEA_EXT_PREFIX}/instruction/v1" +MESSAGE_LEVEL_EXT_URI = f"{DEA_EXT_PREFIX}/messagelevel/v1" + +AGENT_NAME = "dataengineeringagent" +AGENT_BASE_URL_TEMPLATE = ( + "https://geminidataanalytics.googleapis.com/v1/a2a/projects/" + "{project_id}/locations/{location}/agents/{agent_name}" +) + + +# --- Helper Functions --- + + +async def force_alt_sse(request: httpx.Request) -> None: + """HTTP request hook that dynamically forces GFE SSE transcoding for streams.""" + if request.url.path.endswith((":stream", ":subscribe")): + request.url = request.url.copy_merge_params({"alt": "sse"}) + + +def get_bearer_token() -> str | None: + """Fetches a Google Cloud bearer token using Application Default Credentials (ADC).""" + try: + credentials, _ = default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + auth_request = auth_requests.Request() + credentials.refresh(auth_request) + return credentials.token + except Exception as e: # pylint: disable=broad-exception-caught + logging.error("Error getting credentials: %s", e) + logging.info( + "Please ensure you have authenticated with 'gcloud auth" + " application-default login'." + ) + return None + + +def load_instructions(paths: list[str]) -> list[dict[str, str]]: + """Reads instructions from files or directories.""" + instructions = [] + for path_str in paths: + path = pathlib.Path(path_str) + if not path.exists(): + raise FileNotFoundError(f"Instruction path does not exist: {path_str}") + if path.is_file(): + try: + instructions.append( + {"name": path.name, "definition": path.read_text(encoding="utf-8")} + ) + except UnicodeDecodeError: + logging.warning("Skipping non-UTF-8 file: %s", path_str) + elif path.is_dir(): + for file_path in path.iterdir(): + if file_path.is_file() and not file_path.name.startswith("."): + try: + instructions.append({ + "name": file_path.name, + "definition": file_path.read_text(encoding="utf-8"), + }) + except UnicodeDecodeError: + logging.warning("Skipping non-UTF-8 file: %s", file_path) + return instructions + + +def _extract_metadata(event: dict[str, Any]) -> tuple[str | None, str | None]: + """Extracts conversation token and finish reason from an event dictionary.""" + metadata = None + if "task" in event: + metadata = event["task"].get("metadata", {}) + elif "status_update" in event: + metadata = event["status_update"].get("metadata", {}) + elif "message" in event: + metadata = event["message"].get("metadata", {}) + + if not metadata: + return None, None + + conversation_token = None + token_val = metadata.get(CONVERSATION_TOKEN_EXT_URI) + if token_val: + conversation_token = ( + token_val.get("conversationToken") + if isinstance(token_val, dict) + else token_val + ) + + finish_reason = None + reason_val = metadata.get(FINISH_REASON_EXT_URI) + if reason_val: + finish_reason = ( + reason_val.get("finishReason") + if isinstance(reason_val, dict) + else reason_val + ) + + return conversation_token, finish_reason + + +# --- Core Interaction Snippet --- + + +# [START geminidataanalytics_a2a_client] +async def interact_with_data_engineering_agent( + auth_token: str, + gcp_resource_id: str, + message: str, + conversation_token: str | None = None, + instructions: list[dict[str, str]] | None = None, + max_retries: int = 5, +) -> str | None: + """Sends a query to the Data Engineering Agent via the A2A protocol. + + This function implements the complete client communication lifecycle: + 1. Resolves the Agent Card to discover endpoint protocols. + 2. Establishes the A2A client session over HTTP/SSE. + 3. Attaches required metadata extensions (Google Cloud resource ID, Conversation Token, + instructions). + 4. Invokes the agent and streams responses. + 5. Handles DEADLINE_EXCEEDED finish reason by auto-resuming the turn. + + Args: + auth_token: A Google Cloud access token for authentication. + gcp_resource_id: The target Google Cloud resource ID, formatted as + 'projects/{project}/locations/{location}/...' + message: The natural language query to send to the agent. + conversation_token: Optional conversation token to resume an existing + session. + instructions: Optional custom agent instructions. + + Returns: + The updated conversation token for subsequent turns. + """ + # Extract project_id and location from gcp_resource_id + match = re.search(r"projects/([^/]+)/locations/([^/]+)", gcp_resource_id) + if not match: + raise ValueError( + f"Invalid gcp_resource_id format: {gcp_resource_id}. " + "Expected 'projects/{project}/locations/{location}/...'" + ) + project_id, location = match.groups() + agent_base_url = AGENT_BASE_URL_TEMPLATE.format( + project_id=project_id, + location=location, + agent_name=AGENT_NAME, + ) + + headers = {"Authorization": f"Bearer {auth_token}"} + async with httpx.AsyncClient( + headers=headers, + timeout=600.0, + event_hooks={"request": [force_alt_sse]}, + ) as httpx_client: + + # Resolve the Agent Card from the endpoint + resolver = a2a_client.A2ACardResolver( + httpx_client=httpx_client, base_url=agent_base_url + ) + agent_card = await resolver.get_agent_card(relative_card_path="v1/card") + + # Create the A2A protocol client + config = a2a_client.ClientConfig( + supported_protocol_bindings=[a2a_constants.TransportProtocol.HTTP_JSON], + streaming=True, + httpx_client=httpx_client, + ) + client = await a2a_client.create_client( + agent=agent_card, client_config=config + ) + + try: + # Helper to build request metadata + def build_metadata(token_val: str | None) -> dict[str, Any]: + metadata = {GCP_RESOURCE_EXT_URI: {"gcpResourceId": gcp_resource_id}} + if token_val: + metadata[CONVERSATION_TOKEN_EXT_URI] = token_val + if instructions: + metadata[INSTRUCTION_EXT_URI] = {"agentInstructions": instructions} + return metadata + + context_id = str(uuid.uuid4()) + current_token = conversation_token + query_to_send = message + is_retry = False + retry_count = 0 + + extensions = [ + MESSAGE_LEVEL_EXT_URI, + INSTRUCTION_EXT_URI, + GCP_RESOURCE_EXT_URI, + CONVERSATION_TOKEN_EXT_URI, + FINISH_REASON_EXT_URI, + ] + service_params = a2a_service_params.ServiceParametersFactory.create( + [a2a_service_params.with_a2a_extensions(extensions)] + ) + context = a2a_client.ClientCallContext( + service_parameters=service_params, timeout=600.0 + ) + + while True: + if is_retry: + logging.info("Resuming session due to deadline exceeded...") + + user_message = a2a_types.Message( + message_id=str(uuid.uuid4()), + role=a2a_types.Role.ROLE_USER, + parts=[a2a_types.Part(text=query_to_send)], + context_id=context_id, + ) + + request = a2a_types.SendMessageRequest( + message=user_message, + metadata=build_metadata(current_token), + ) + + finish_reason: str | None = None + responses = client.send_message(request, context=context) + + async for response_obj in responses: + # Process and print outcomes to console + # (teaches user how to handle output) + print(response_obj) + event = pb_json_format.MessageToDict( + response_obj, preserving_proto_field_name=True + ) + events = event if isinstance(event, list) else [event] + for e in events: + token, reason = _extract_metadata(e) + current_token = token or current_token + finish_reason = reason or finish_reason + + # Handle deadline exceeded by repeating the turn + if finish_reason == "DEADLINE_EXCEEDED": + retry_count += 1 + if retry_count > max_retries: + logging.error( + "Max retries (%d) exceeded due to repeated DEADLINE_EXCEEDED.", + max_retries, + ) + break + logging.warning("Agent execution hit deadline. Resuming...") + query_to_send = "Please continue." + is_retry = True + else: + break + + return current_token + + finally: + await client.close() +# [END geminidataanalytics_a2a_client] + + +# --- Main CLI Entrypoint --- + + +async def main() -> None: + parser = argparse.ArgumentParser( + description="Data Engineering Agent A2A Client" + ) + parser.add_argument( + "--gcp_resource_id", required=True, help="Target Google Cloud resource ID." + ) + parser.add_argument("--message", required=True, help="The message to send.") + parser.add_argument( + "--conversation_token_path", + help="Local file to persist conversation token.", + ) + parser.add_argument( + "--instruction_path", + action="append", + help="Path to instruction files/dirs.", + ) + + args = parser.parse_args() + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("a2a").setLevel(logging.WARNING) + + token = os.getenv("GOOGLE_ACCESS_TOKEN") or get_bearer_token() + if not token: + logging.error("Authentication failed. No Google access token found.") + return + + conversation_token = None + if args.conversation_token_path: + token_file = pathlib.Path(args.conversation_token_path) + if token_file.exists(): + conversation_token = token_file.read_text(encoding="utf-8").strip() + + try: + instructions = load_instructions(args.instruction_path or []) + new_token = await interact_with_data_engineering_agent( + auth_token=token, + gcp_resource_id=args.gcp_resource_id, + message=args.message, + conversation_token=conversation_token, + instructions=instructions, + ) + + if args.conversation_token_path and new_token: + pathlib.Path(args.conversation_token_path).write_text( + new_token, encoding="utf-8" + ) + + except Exception as e: # pylint: disable=broad-exception-caught + logging.exception("Operation failed: %s", e) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass diff --git a/geminidataanalytics/dataengineeringagent/dea_a2a_client_test.py b/geminidataanalytics/dataengineeringagent/dea_a2a_client_test.py new file mode 100644 index 00000000000..1be0c99ee73 --- /dev/null +++ b/geminidataanalytics/dataengineeringagent/dea_a2a_client_test.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python + +# 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. + +"""Unit tests for the Data Engineering Agent A2A client.""" + +import unittest +from unittest import mock + +import dea_a2a_client + + +class DeaA2AClientTest(unittest.IsolatedAsyncioTestCase): + + @mock.patch("dea_a2a_client.default") + @mock.patch("dea_a2a_client.auth_requests.Request") + def test_get_bearer_token_success(self, unused_auth_request, mock_default): + mock_creds = mock.MagicMock() + mock_creds.token = "test-token" + mock_default.return_value = (mock_creds, "test-project") + token = dea_a2a_client.get_bearer_token() + self.assertEqual(token, "test-token") + mock_creds.refresh.assert_called_once() + + @mock.patch("dea_a2a_client.default") + def test_get_bearer_token_failure(self, mock_default): + mock_default.side_effect = Exception("Auth failed") + token = dea_a2a_client.get_bearer_token() + self.assertIsNone(token) + + @mock.patch("dea_a2a_client.a2a_client.A2ACardResolver") + @mock.patch("dea_a2a_client.a2a_client.create_client") + @mock.patch("httpx.AsyncClient") + @mock.patch("dea_a2a_client.pb_json_format.MessageToDict") + async def test_multi_turn_and_retry( + self, + mock_message_to_dict, + mock_httpx_cls, + mock_create_client, + mock_resolver_cls, + ): + mock_message_to_dict.side_effect = lambda x, **kwargs: x + + mock_card = mock.MagicMock() + mock_card.name = "DEA" + mock_interface = mock.MagicMock() + mock_card.supported_interfaces = [mock_interface] + + mock_resolver = mock_resolver_cls.return_value + mock_resolver.get_agent_card = mock.AsyncMock(return_value=mock_card) + + mock_client = mock.MagicMock() + mock_client.close = mock.AsyncMock() + mock_create_client.return_value = mock_client + + # 3. Define Response Behavior + # Turn 1: Returns DEADLINE_EXCEEDED and a token + token_v1 = "token-v1" + response1 = { + "task": { + "id": "t1", + "metadata": { + dea_a2a_client.CONVERSATION_TOKEN_EXT_URI: { + "conversationToken": token_v1 + }, + dea_a2a_client.FINISH_REASON_EXT_URI: { + "finishReason": "DEADLINE_EXCEEDED" + }, + }, + "history": [ + {"role": "agent", "content": {"text": "Part 1 of response"}} + ], + } + } + + # Turn 2 (Retry of Turn 1): Returns FINISHED + response2 = { + "task": { + "id": "t2", + "metadata": { + dea_a2a_client.CONVERSATION_TOKEN_EXT_URI: { + "conversationToken": "token-v2" + }, + dea_a2a_client.FINISH_REASON_EXT_URI: { + "finishReason": "FINISHED" + }, + }, + "history": [ + {"role": "agent", "content": {"text": "Part 2 of response"}} + ], + } + } + + class AsyncIter: + + def __init__(self, items): + self.items = items + + def __aiter__(self): + return self + + async def __anext__(self): + if not self.items: + raise StopAsyncIteration + return self.items.pop(0) + + mock_client.send_message.side_effect = [ + AsyncIter([response1]), + AsyncIter([response2]), + ] + + token = "fake-token" + gcp_res_id = "projects/tp/locations/tl/repositories/tr/workspaces/default" + result_token = await dea_a2a_client.interact_with_data_engineering_agent( + auth_token=token, + gcp_resource_id=gcp_res_id, + message="Initial Query", + ) + + self.assertEqual(result_token, "token-v2") + + # Verify Authorization header was passed in httpx client initialization + mock_httpx_cls.assert_called_once() + called_headers = mock_httpx_cls.call_args[1].get("headers") + self.assertEqual(called_headers, {"Authorization": f"Bearer {token}"}) + + @mock.patch("dea_a2a_client.a2a_client.A2ACardResolver") + @mock.patch("dea_a2a_client.a2a_client.create_client") + @mock.patch("httpx.AsyncClient") + @mock.patch("dea_a2a_client.pb_json_format.MessageToDict") + async def test_session_persistence( + self, + mock_message_to_dict, + unused_httpx, + mock_create_client, + mock_resolver_cls, + ): + mock_message_to_dict.side_effect = lambda x, **kwargs: x + + mock_client = mock.MagicMock() + mock_client.close = mock.AsyncMock() + mock_create_client.return_value = mock_client + + mock_card = mock.MagicMock() + mock_card.supported_interfaces = [mock.MagicMock()] + mock_resolver_cls.return_value.get_agent_card = mock.AsyncMock( + return_value=mock_card + ) + + response = {"message": {"role": "agent", "content": {"text": "response"}}} + + class AsyncIter: + + def __init__(self, item): + self.item = item + self.used = False + + def __aiter__(self): + return self + + async def __anext__(self): + if self.used: + raise StopAsyncIteration + self.used = True + return self.item + + mock_client.send_message.return_value = AsyncIter(response) + + token = "fake-token" + gcp_resource_id = "projects/p/locations/l/repositories/r/workspaces/w" + existing_token = "old-token" + await dea_a2a_client.interact_with_data_engineering_agent( + auth_token=token, + gcp_resource_id=gcp_resource_id, + message="query", + conversation_token=existing_token, + ) + call_request = mock_client.send_message.call_args[0][0] + self.assertEqual( + call_request.metadata[dea_a2a_client.CONVERSATION_TOKEN_EXT_URI], + existing_token, + ) + + @mock.patch("dea_a2a_client.a2a_client.A2ACardResolver") + @mock.patch("dea_a2a_client.a2a_client.create_client") + @mock.patch("httpx.AsyncClient") + @mock.patch("dea_a2a_client.pb_json_format.MessageToDict") + async def test_retry_limit_exceeded( + self, + mock_message_to_dict, + unused_httpx, + mock_create_client, + mock_resolver_cls, + ): + mock_message_to_dict.side_effect = lambda x, **kwargs: x + + mock_card = mock.MagicMock() + mock_card.supported_interfaces = [mock.MagicMock()] + mock_resolver_cls.return_value.get_agent_card = mock.AsyncMock( + return_value=mock_card + ) + + mock_client = mock.MagicMock() + mock_client.close = mock.AsyncMock() + mock_create_client.return_value = mock_client + response = { + "task": { + "id": "t1", + "metadata": { + dea_a2a_client.FINISH_REASON_EXT_URI: { + "finishReason": "DEADLINE_EXCEEDED" + }, + }, + "history": [{"role": "agent", "content": {"text": "Waiting..."}}], + } + } + + class AsyncIter: + + def __init__(self, item): + self.item = item + self.used = False + + def __aiter__(self): + return self + + async def __anext__(self): + if self.used: + raise StopAsyncIteration + self.used = True + return self.item + + mock_client.send_message.side_effect = lambda *args, **kwargs: AsyncIter(response) + + token = "fake-token" + gcp_res_id = "projects/tp/locations/tl/repositories/tr/workspaces/default" + result_token = await dea_a2a_client.interact_with_data_engineering_agent( + auth_token=token, + gcp_resource_id=gcp_res_id, + message="Initial Query", + max_retries=2, + ) + + self.assertIsNone(result_token) + self.assertEqual(mock_client.send_message.call_count, 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/geminidataanalytics/dataengineeringagent/requirements.txt b/geminidataanalytics/dataengineeringagent/requirements.txt new file mode 100644 index 00000000000..5630fd4ffc7 --- /dev/null +++ b/geminidataanalytics/dataengineeringagent/requirements.txt @@ -0,0 +1,3 @@ +a2a-sdk==1.0.3 +httpx==0.28.1 +google-auth==2.52.0 diff --git a/gemma2/noxfile_config.py b/gemma2/noxfile_config.py index 494cf15318d..fd69813938c 100644 --- a/gemma2/noxfile_config.py +++ b/gemma2/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/gemma2/requirements-test.txt b/gemma2/requirements-test.txt index 40543aababf..b3b2b3ca4cc 100644 --- a/gemma2/requirements-test.txt +++ b/gemma2/requirements-test.txt @@ -1 +1 @@ -pytest==8.3.3 +pytest==9.0.3 diff --git a/gemma2/requirements.txt b/gemma2/requirements.txt index f8990233d3f..67a5814ea6b 100644 --- a/gemma2/requirements.txt +++ b/gemma2/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-aiplatform[all]==1.64.0 -protobuf==5.29.5 +google-cloud-aiplatform==1.158.0 +protobuf==6.33.6 diff --git a/genai/batch_prediction/batchpredict_embeddings_with_gcs.py b/genai/batch_prediction/batchpredict_embeddings_with_gcs.py index 41420db3141..4fb8148e9f5 100644 --- a/genai/batch_prediction/batchpredict_embeddings_with_gcs.py +++ b/genai/batch_prediction/batchpredict_embeddings_with_gcs.py @@ -34,7 +34,7 @@ def generate_content(output_uri: str) -> str: print(f"Job name: {job.name}") print(f"Job state: {job.state}") # Example response: - # Job name: projects/%PROJECT_ID%/locations/us-central1/batchPredictionJobs/9876453210000000000 + # Job name: projects/.../locations/.../batchPredictionJobs/9876453210000000000 # Job state: JOB_STATE_PENDING # See the documentation: https://googleapis.github.io/python-genai/genai.html#genai.types.BatchJob diff --git a/genai/batch_prediction/batchpredict_with_bq.py b/genai/batch_prediction/batchpredict_with_bq.py index 30ea7c4a90f..2fc6dab4a7c 100644 --- a/genai/batch_prediction/batchpredict_with_bq.py +++ b/genai/batch_prediction/batchpredict_with_bq.py @@ -18,7 +18,7 @@ def generate_content(output_uri: str) -> str: import time from google import genai - from google.genai.types import CreateBatchJobConfig, JobState, HttpOptions + from google.genai.types import CreateBatchJobConfig, HttpOptions, JobState client = genai.Client(http_options=HttpOptions(api_version="v1")) @@ -28,14 +28,14 @@ def generate_content(output_uri: str) -> str: job = client.batches.create( # To use a tuned model, set the model param to your tuned model using the following format: # model="projects/{PROJECT_ID}/locations/{LOCATION}/models/{MODEL_ID} - model="gemini-2.5-flash", + model="gemini-3.5-flash", src="bq://storage-samples.generative_ai.batch_requests_for_multimodal_input", config=CreateBatchJobConfig(dest=output_uri), ) print(f"Job name: {job.name}") print(f"Job state: {job.state}") # Example response: - # Job name: projects/%PROJECT_ID%/locations/us-central1/batchPredictionJobs/9876453210000000000 + # Job name: projects/.../locations/.../batchPredictionJobs/9876453210000000000 # Job state: JOB_STATE_PENDING # See the documentation: https://googleapis.github.io/python-genai/genai.html#genai.types.BatchJob diff --git a/genai/batch_prediction/batchpredict_with_gcs.py b/genai/batch_prediction/batchpredict_with_gcs.py index 75061405ff0..2617b123231 100644 --- a/genai/batch_prediction/batchpredict_with_gcs.py +++ b/genai/batch_prediction/batchpredict_with_gcs.py @@ -18,7 +18,7 @@ def generate_content(output_uri: str) -> str: import time from google import genai - from google.genai.types import CreateBatchJobConfig, JobState, HttpOptions + from google.genai.types import CreateBatchJobConfig, HttpOptions, JobState client = genai.Client(http_options=HttpOptions(api_version="v1")) # TODO(developer): Update and un-comment below line @@ -28,7 +28,7 @@ def generate_content(output_uri: str) -> str: job = client.batches.create( # To use a tuned model, set the model param to your tuned model using the following format: # model="projects/{PROJECT_ID}/locations/{LOCATION}/models/{MODEL_ID} - model="gemini-2.5-flash", + model="gemini-3.5-flash", # Source link: https://storage.cloud.google.com/cloud-samples-data/batch/prompt_for_batch_gemini_predict.jsonl src="gs://cloud-samples-data/batch/prompt_for_batch_gemini_predict.jsonl", config=CreateBatchJobConfig(dest=output_uri), @@ -36,7 +36,7 @@ def generate_content(output_uri: str) -> str: print(f"Job name: {job.name}") print(f"Job state: {job.state}") # Example response: - # Job name: projects/%PROJECT_ID%/locations/us-central1/batchPredictionJobs/9876453210000000000 + # Job name: projects/.../locations/.../batchPredictionJobs/9876453210000000000 # Job state: JOB_STATE_PENDING # See the documentation: https://googleapis.github.io/python-genai/genai.html#genai.types.BatchJob diff --git a/genai/batch_prediction/get_batch_job.py b/genai/batch_prediction/get_batch_job.py index a17b0457e25..c6e0453da64 100644 --- a/genai/batch_prediction/get_batch_job.py +++ b/genai/batch_prediction/get_batch_job.py @@ -23,7 +23,7 @@ def get_batch_job(batch_job_name: str) -> types.BatchJob: client = genai.Client(http_options=HttpOptions(api_version="v1")) # Get the batch job - # Eg. batch_job_name = "projects/123456789012/locations/us-central1/batchPredictionJobs/1234567890123456789" +# Eg. batch_job_name = "projects/123456789012/locations/.../batchPredictionJobs/1234567890123456789" batch_job = client.batches.get(name=batch_job_name) print(f"Job state: {batch_job.state}") diff --git a/genai/batch_prediction/noxfile_config.py b/genai/batch_prediction/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/batch_prediction/noxfile_config.py +++ b/genai/batch_prediction/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/batch_prediction/requirements-test.txt b/genai/batch_prediction/requirements-test.txt index 937db8fb0d5..22a9617b8e8 100644 --- a/genai/batch_prediction/requirements-test.txt +++ b/genai/batch_prediction/requirements-test.txt @@ -1,4 +1,2 @@ google-api-core==2.24.0 -google-cloud-bigquery==3.29.0 -google-cloud-storage==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/batch_prediction/requirements.txt b/genai/batch_prediction/requirements.txt index 7ad844cebcf..4f44a6593bb 100644 --- a/genai/batch_prediction/requirements.txt +++ b/genai/batch_prediction/requirements.txt @@ -1 +1,3 @@ -google-genai==1.27.0 +google-cloud-bigquery==3.29.0 +google-cloud-storage==2.19.0 +google-genai==1.42.0 diff --git a/genai/batch_prediction/test_batch_prediction_examples.py b/genai/batch_prediction/test_batch_prediction_examples.py index 5183161be65..5079dfd2cd0 100644 --- a/genai/batch_prediction/test_batch_prediction_examples.py +++ b/genai/batch_prediction/test_batch_prediction_examples.py @@ -11,19 +11,10 @@ # 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. - -# -# Using Google Cloud Vertex AI to test the code samples. -# -from datetime import datetime as dt -import os - from unittest.mock import MagicMock, patch -from google.cloud import bigquery, storage from google.genai import types from google.genai.types import JobState -import pytest import batchpredict_embeddings_with_gcs import batchpredict_with_bq @@ -31,67 +22,113 @@ import get_batch_job -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" -os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" -# The project name is included in the CICD pipeline -# os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" -BQ_OUTPUT_DATASET = f"{os.environ['GOOGLE_CLOUD_PROJECT']}.gen_ai_batch_prediction" -GCS_OUTPUT_BUCKET = "python-docs-samples-tests" - - -@pytest.fixture(scope="session") -def bq_output_uri() -> str: - table_name = f"text_output_{dt.now().strftime('%Y_%m_%d_T%H_%M_%S')}" - table_uri = f"{BQ_OUTPUT_DATASET}.{table_name}" +@patch("google.genai.Client") +@patch("time.sleep", return_value=None) +def test_batch_prediction_embeddings_with_gcs( + mock_sleep: MagicMock, mock_genai_client: MagicMock +) -> None: + # Mock the API response + mock_batch_job_running = types.BatchJob( + name="test-batch-job", state="JOB_STATE_RUNNING" + ) + mock_batch_job_succeeded = types.BatchJob( + name="test-batch-job", state="JOB_STATE_SUCCEEDED" + ) - yield f"bq://{table_uri}" + mock_genai_client.return_value.batches.create.return_value = ( + mock_batch_job_running + ) + mock_genai_client.return_value.batches.get.return_value = ( + mock_batch_job_succeeded + ) - bq_client = bigquery.Client() - bq_client.delete_table(table_uri, not_found_ok=True) + response = batchpredict_embeddings_with_gcs.generate_content( + output_uri="gs://test-bucket/test-prefix" + ) + mock_genai_client.assert_called_once_with( + http_options=types.HttpOptions(api_version="v1") + ) + mock_genai_client.return_value.batches.create.assert_called_once() + mock_genai_client.return_value.batches.get.assert_called_once() + assert response == JobState.JOB_STATE_SUCCEEDED -@pytest.fixture(scope="session") -def gcs_output_uri() -> str: - prefix = f"text_output/{dt.now()}" - yield f"gs://{GCS_OUTPUT_BUCKET}/{prefix}" +@patch("google.genai.Client") +@patch("time.sleep", return_value=None) +def test_batch_prediction_with_bq( + mock_sleep: MagicMock, mock_genai_client: MagicMock +) -> None: + # Mock the API response + mock_batch_job_running = types.BatchJob( + name="test-batch-job", state="JOB_STATE_RUNNING" + ) + mock_batch_job_succeeded = types.BatchJob( + name="test-batch-job", state="JOB_STATE_SUCCEEDED" + ) - storage_client = storage.Client() - bucket = storage_client.get_bucket(GCS_OUTPUT_BUCKET) - blobs = bucket.list_blobs(prefix=prefix) - for blob in blobs: - blob.delete() + mock_genai_client.return_value.batches.create.return_value = ( + mock_batch_job_running + ) + mock_genai_client.return_value.batches.get.return_value = ( + mock_batch_job_succeeded + ) + response = batchpredict_with_bq.generate_content( + output_uri="bq://test-project.test_dataset.test_table" + ) -def test_batch_prediction_embeddings_with_gcs(gcs_output_uri: str) -> None: - response = batchpredict_embeddings_with_gcs.generate_content( - output_uri=gcs_output_uri + mock_genai_client.assert_called_once_with( + http_options=types.HttpOptions(api_version="v1") ) + mock_genai_client.return_value.batches.create.assert_called_once() + mock_genai_client.return_value.batches.get.assert_called_once() assert response == JobState.JOB_STATE_SUCCEEDED -def test_batch_prediction_with_bq(bq_output_uri: str) -> None: - response = batchpredict_with_bq.generate_content(output_uri=bq_output_uri) - assert response == JobState.JOB_STATE_SUCCEEDED +@patch("google.genai.Client") +@patch("time.sleep", return_value=None) +def test_batch_prediction_with_gcs( + mock_sleep: MagicMock, mock_genai_client: MagicMock +) -> None: + # Mock the API response + mock_batch_job_running = types.BatchJob( + name="test-batch-job", state="JOB_STATE_RUNNING" + ) + mock_batch_job_succeeded = types.BatchJob( + name="test-batch-job", state="JOB_STATE_SUCCEEDED" + ) + mock_genai_client.return_value.batches.create.return_value = ( + mock_batch_job_running + ) + mock_genai_client.return_value.batches.get.return_value = ( + mock_batch_job_succeeded + ) + + response = batchpredict_with_gcs.generate_content( + output_uri="gs://test-bucket/test-prefix" + ) -def test_batch_prediction_with_gcs(gcs_output_uri: str) -> None: - response = batchpredict_with_gcs.generate_content(output_uri=gcs_output_uri) + mock_genai_client.assert_called_once_with( + http_options=types.HttpOptions(api_version="v1") + ) + mock_genai_client.return_value.batches.create.assert_called_once() + mock_genai_client.return_value.batches.get.assert_called_once() assert response == JobState.JOB_STATE_SUCCEEDED @patch("google.genai.Client") def test_get_batch_job(mock_genai_client: MagicMock) -> None: # Mock the API response - mock_batch_job = types.BatchJob( - name="test-batch-job", - state="JOB_STATE_PENDING" - ) + mock_batch_job = types.BatchJob(name="test-batch-job", state="JOB_STATE_PENDING") mock_genai_client.return_value.batches.get.return_value = mock_batch_job response = get_batch_job.get_batch_job("test-batch-job") - mock_genai_client.assert_called_once_with(http_options=types.HttpOptions(api_version="v1")) + mock_genai_client.assert_called_once_with( + http_options=types.HttpOptions(api_version="v1") + ) mock_genai_client.return_value.batches.get.assert_called_once() assert response == mock_batch_job diff --git a/genai/bounding_box/boundingbox_with_txt_img.py b/genai/bounding_box/boundingbox_with_txt_img.py index a22f15dc664..679c9be0fae 100644 --- a/genai/bounding_box/boundingbox_with_txt_img.py +++ b/genai/bounding_box/boundingbox_with_txt_img.py @@ -102,7 +102,7 @@ def plot_bounding_boxes(image_uri: str, bounding_boxes: list[BoundingBox]) -> No image_uri = "https://storage.googleapis.com/generativeai-downloads/images/socks.jpg" response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ Part.from_uri( file_uri=image_uri, diff --git a/genai/bounding_box/noxfile_config.py b/genai/bounding_box/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/bounding_box/noxfile_config.py +++ b/genai/bounding_box/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/bounding_box/requirements-test.txt b/genai/bounding_box/requirements-test.txt index e43b7792721..22a9617b8e8 100644 --- a/genai/bounding_box/requirements-test.txt +++ b/genai/bounding_box/requirements-test.txt @@ -1,2 +1,2 @@ google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/bounding_box/requirements.txt b/genai/bounding_box/requirements.txt index 661c889ab37..75a39f7e582 100644 --- a/genai/bounding_box/requirements.txt +++ b/genai/bounding_box/requirements.txt @@ -1,2 +1,2 @@ -google-genai==1.27.0 -pillow==11.1.0 +google-genai==1.42.0 +pillow==12.3.0; python_version >= "3.10" diff --git a/genai/bounding_box/test_bounding_box_examples.py b/genai/bounding_box/test_bounding_box_examples.py index bb6eca92008..39b0056e7a3 100644 --- a/genai/bounding_box/test_bounding_box_examples.py +++ b/genai/bounding_box/test_bounding_box_examples.py @@ -20,7 +20,7 @@ import boundingbox_with_txt_img -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "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" 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/generative_ai/provisioned_throughput/noxfile_config.py b/genai/code_execution/noxfile_config.py similarity index 94% rename from generative_ai/provisioned_throughput/noxfile_config.py rename to genai/code_execution/noxfile_config.py index 962ba40a926..650b3c47840 100644 --- a/generative_ai/provisioned_throughput/noxfile_config.py +++ b/genai/code_execution/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2021 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,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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/code_execution/requirements-test.txt b/genai/code_execution/requirements-test.txt new file mode 100644 index 00000000000..7289efe2596 --- /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.3; python_version >= "3.10" +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..40e835cf14b --- /dev/null +++ b/genai/code_execution/requirements.txt @@ -0,0 +1,2 @@ +google-genai==1.60.0 +pillow==12.3.0; python_version >= "3.10" diff --git a/genai/code_execution/test_codeexecution.py b/genai/code_execution/test_codeexecution.py new file mode 100644 index 00000000000..ea978643c7c --- /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_ENTERPRISE"] = "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/content_cache/contentcache_create_with_txt_gcs_pdf.py b/genai/content_cache/contentcache_create_with_txt_gcs_pdf.py index 1e158d940c4..56f04011e56 100644 --- a/genai/content_cache/contentcache_create_with_txt_gcs_pdf.py +++ b/genai/content_cache/contentcache_create_with_txt_gcs_pdf.py @@ -42,12 +42,12 @@ def create_content_cache() -> str: ] content_cache = client.caches.create( - model="gemini-2.5-flash", + model="gemini-3.5-flash", config=CreateCachedContentConfig( contents=contents, system_instruction=system_instruction, # (Optional) For enhanced security, the content cache can be encrypted using a Cloud KMS key - # kms_key_name = "projects/.../locations/us-central1/keyRings/.../cryptoKeys/..." + # kms_key_name = "projects/.../locations/.../keyRings/.../cryptoKeys/..." display_name="example-cache", ttl="86400s", ), @@ -56,7 +56,7 @@ def create_content_cache() -> str: print(content_cache.name) print(content_cache.usage_metadata) # Example response: - # projects/111111111111/locations/us-central1/cachedContents/1111111111111111111 + # projects/111111111111/locations/.../cachedContents/1111111111111111111 # CachedContentUsageMetadata(audio_duration_seconds=None, image_count=167, # text_count=153, total_token_count=43130, video_duration_seconds=None) # [END googlegenaisdk_contentcache_create_with_txt_gcs_pdf] diff --git a/genai/content_cache/contentcache_delete.py b/genai/content_cache/contentcache_delete.py index 3761b84ea6a..9afe8962a5a 100644 --- a/genai/content_cache/contentcache_delete.py +++ b/genai/content_cache/contentcache_delete.py @@ -19,11 +19,11 @@ def delete_context_caches(cache_name: str) -> str: client = genai.Client() # Delete content cache using name - # E.g cache_name = 'projects/111111111111/locations/us-central1/cachedContents/1111111111111111111' + # E.g cache_name = 'projects/111111111111/locations/.../cachedContents/1111111111111111111' client.caches.delete(name=cache_name) print("Deleted Cache", cache_name) # Example response - # Deleted Cache projects/111111111111/locations/us-central1/cachedContents/1111111111111111111 + # Deleted Cache projects/111111111111/locations/.../cachedContents/1111111111111111111 # [END googlegenaisdk_contentcache_delete] return cache_name diff --git a/genai/content_cache/contentcache_list.py b/genai/content_cache/contentcache_list.py index f477da31b29..9f0f2a6b510 100644 --- a/genai/content_cache/contentcache_list.py +++ b/genai/content_cache/contentcache_list.py @@ -29,8 +29,8 @@ def list_context_caches() -> str: print(f"Expires at: {content_cache.expire_time}") # Example response: - # * Cache `projects/111111111111/locations/us-central1/cachedContents/1111111111111111111` for - # model `projects/111111111111/locations/us-central1/publishers/google/models/gemini-XXX-pro-XXX` + # * Cache `projects/111111111111/locations/.../cachedContents/1111111111111111111` for + # model `projects/111111111111/locations/.../publishers/google/models/gemini-XXX-pro-XXX` # * Last updated at: 2025-02-13 14:46:42.620490+00:00 # * CachedContentUsageMetadata(audio_duration_seconds=None, image_count=167, text_count=153, total_token_count=43130, video_duration_seconds=None) # ... diff --git a/genai/content_cache/contentcache_update.py b/genai/content_cache/contentcache_update.py index 1f1136359be..27f96743385 100644 --- a/genai/content_cache/contentcache_update.py +++ b/genai/content_cache/contentcache_update.py @@ -25,7 +25,7 @@ def update_content_cache(cache_name: str) -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) # Get content cache by name - # cache_name = "projects/111111111111/locations/us-central1/cachedContents/1111111111111111111" + # cache_name = "projects/.../locations/.../cachedContents/1111111111111111111" content_cache = client.caches.get(name=cache_name) print("Expire time", content_cache.expire_time) # Example response diff --git a/genai/content_cache/contentcache_use_with_txt.py b/genai/content_cache/contentcache_use_with_txt.py index 488d5c763af..97c0d662d03 100644 --- a/genai/content_cache/contentcache_use_with_txt.py +++ b/genai/content_cache/contentcache_use_with_txt.py @@ -20,9 +20,9 @@ def generate_content(cache_name: str) -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) # Use content cache to generate text response - # E.g cache_name = 'projects/111111111111/locations/us-central1/cachedContents/1111111111111111111' + # E.g cache_name = 'projects/.../locations/.../cachedContents/1111111111111111111' response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="Summarize the pdfs", config=GenerateContentConfig( cached_content=cache_name, diff --git a/genai/content_cache/noxfile_config.py b/genai/content_cache/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/content_cache/noxfile_config.py +++ b/genai/content_cache/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/content_cache/requirements-test.txt b/genai/content_cache/requirements-test.txt index e43b7792721..22a9617b8e8 100644 --- a/genai/content_cache/requirements-test.txt +++ b/genai/content_cache/requirements-test.txt @@ -1,2 +1,2 @@ google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/content_cache/requirements.txt b/genai/content_cache/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/content_cache/requirements.txt +++ b/genai/content_cache/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/content_cache/test_content_cache_examples.py b/genai/content_cache/test_content_cache_examples.py index d7d9e5abda4..d17c43b1bea 100644 --- a/genai/content_cache/test_content_cache_examples.py +++ b/genai/content_cache/test_content_cache_examples.py @@ -21,7 +21,7 @@ import contentcache_use_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" diff --git a/genai/controlled_generation/ctrlgen_with_class_schema.py b/genai/controlled_generation/ctrlgen_with_class_schema.py index 8613c206a59..51effae1c57 100644 --- a/genai/controlled_generation/ctrlgen_with_class_schema.py +++ b/genai/controlled_generation/ctrlgen_with_class_schema.py @@ -17,7 +17,6 @@ def generate_content() -> str: # [START googlegenaisdk_ctrlgen_with_class_schema] from google import genai from google.genai.types import GenerateContentConfig, HttpOptions - from pydantic import BaseModel class Recipe(BaseModel): @@ -26,7 +25,7 @@ class Recipe(BaseModel): client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="List a few popular cookie recipes.", config=GenerateContentConfig( response_mime_type="application/json", diff --git a/genai/controlled_generation/ctrlgen_with_enum_class_schema.py b/genai/controlled_generation/ctrlgen_with_enum_class_schema.py index 0eeb869c200..9eb8a839bf3 100644 --- a/genai/controlled_generation/ctrlgen_with_enum_class_schema.py +++ b/genai/controlled_generation/ctrlgen_with_enum_class_schema.py @@ -29,7 +29,7 @@ class InstrumentClass(enum.Enum): client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="What type of instrument is a guitar?", config={ "response_mime_type": "text/x.enum", diff --git a/genai/controlled_generation/ctrlgen_with_enum_schema.py b/genai/controlled_generation/ctrlgen_with_enum_schema.py index 3cfd358ac25..9a2493a5b46 100644 --- a/genai/controlled_generation/ctrlgen_with_enum_schema.py +++ b/genai/controlled_generation/ctrlgen_with_enum_schema.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="What type of instrument is an oboe?", config=GenerateContentConfig( response_mime_type="text/x.enum", diff --git a/genai/controlled_generation/ctrlgen_with_nested_class_schema.py b/genai/controlled_generation/ctrlgen_with_nested_class_schema.py index 633c79bb128..970f11a2231 100644 --- a/genai/controlled_generation/ctrlgen_with_nested_class_schema.py +++ b/genai/controlled_generation/ctrlgen_with_nested_class_schema.py @@ -19,7 +19,6 @@ def generate_content() -> str: from google import genai from google.genai.types import GenerateContentConfig, HttpOptions - from pydantic import BaseModel class Grade(enum.Enum): @@ -36,7 +35,7 @@ class Recipe(BaseModel): client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="List about 10 home-baked cookies and give them grades based on tastiness.", config=GenerateContentConfig( response_mime_type="application/json", diff --git a/genai/controlled_generation/ctrlgen_with_nullable_schema.py b/genai/controlled_generation/ctrlgen_with_nullable_schema.py index 8aba542425e..9f93ea465bb 100644 --- a/genai/controlled_generation/ctrlgen_with_nullable_schema.py +++ b/genai/controlled_generation/ctrlgen_with_nullable_schema.py @@ -51,7 +51,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=prompt, config=GenerateContentConfig( response_mime_type="application/json", diff --git a/genai/controlled_generation/ctrlgen_with_resp_schema.py b/genai/controlled_generation/ctrlgen_with_resp_schema.py index 2e17c516d0f..33e78d1ae00 100644 --- a/genai/controlled_generation/ctrlgen_with_resp_schema.py +++ b/genai/controlled_generation/ctrlgen_with_resp_schema.py @@ -36,7 +36,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=prompt, config={ "response_mime_type": "application/json", diff --git a/genai/controlled_generation/noxfile_config.py b/genai/controlled_generation/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/controlled_generation/noxfile_config.py +++ b/genai/controlled_generation/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/controlled_generation/requirements-test.txt b/genai/controlled_generation/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/genai/controlled_generation/requirements-test.txt +++ b/genai/controlled_generation/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/genai/controlled_generation/requirements.txt b/genai/controlled_generation/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/controlled_generation/requirements.txt +++ b/genai/controlled_generation/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/controlled_generation/test_controlled_generation_examples.py b/genai/controlled_generation/test_controlled_generation_examples.py index ab27d8e7a46..2bd518be054 100644 --- a/genai/controlled_generation/test_controlled_generation_examples.py +++ b/genai/controlled_generation/test_controlled_generation_examples.py @@ -25,7 +25,7 @@ import ctrlgen_with_nullable_schema import ctrlgen_with_resp_schema -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "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" diff --git a/genai/count_tokens/counttoken_compute_with_txt.py b/genai/count_tokens/counttoken_compute_with_txt.py index 0b3af0a6bb2..088f437c659 100644 --- a/genai/count_tokens/counttoken_compute_with_txt.py +++ b/genai/count_tokens/counttoken_compute_with_txt.py @@ -20,7 +20,7 @@ def compute_tokens_example() -> int: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.compute_tokens( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="What's the longest word in the English language?", ) diff --git a/genai/count_tokens/counttoken_localtokenizer_compute_with_txt.py b/genai/count_tokens/counttoken_localtokenizer_compute_with_txt.py new file mode 100644 index 00000000000..f89f323a6b2 --- /dev/null +++ b/genai/count_tokens/counttoken_localtokenizer_compute_with_txt.py @@ -0,0 +1,36 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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 counttoken_localtokenizer_compute_with_txt() -> int: + # [START googlegenaisdk_counttoken_localtokenizer_compute_with_txt] + from google.genai.local_tokenizer import LocalTokenizer + + tokenizer = LocalTokenizer(model_name="gemini-3.5-flash") + response = tokenizer.compute_tokens("What's the longest word in the English language?") + print(response) + # Example output: + # tokens_info=[TokensInfo( + # role='user', + # token_ids=[3689, 236789, 236751, 506, + # 27801, 3658, 528, 506, 5422, 5192, 236881], + # tokens=[b'What', b"'", b's', b' the', b' longest', + # b' word', b' in', b' the', b' English', b' language', b'?'] + # )] + # [END googlegenaisdk_counttoken_localtokenizer_compute_with_txt] + return response.tokens_info + + +if __name__ == "__main__": + counttoken_localtokenizer_compute_with_txt() diff --git a/genai/count_tokens/counttoken_localtokenizer_with_txt.py b/genai/count_tokens/counttoken_localtokenizer_with_txt.py new file mode 100644 index 00000000000..f726303cb71 --- /dev/null +++ b/genai/count_tokens/counttoken_localtokenizer_with_txt.py @@ -0,0 +1,30 @@ +# 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 counttoken_localtokenizer_with_txt() -> int: + # [START googlegenaisdk_counttoken_localtokenizer_with_txt] + from google.genai.local_tokenizer import LocalTokenizer + + tokenizer = LocalTokenizer(model_name="gemini-3.5-flash") + response = tokenizer.count_tokens("What's the highest mountain in Africa?") + print(response) + # Example output: + # total_tokens=10 + # [END googlegenaisdk_counttoken_localtokenizer_with_txt] + return response.total_tokens + + +if __name__ == "__main__": + counttoken_localtokenizer_with_txt() diff --git a/genai/count_tokens/counttoken_resp_with_txt.py b/genai/count_tokens/counttoken_resp_with_txt.py index f2db5309e01..f5cb315effe 100644 --- a/genai/count_tokens/counttoken_resp_with_txt.py +++ b/genai/count_tokens/counttoken_resp_with_txt.py @@ -24,7 +24,7 @@ def count_tokens_example() -> int: # Send text to Gemini response = client.models.generate_content( - model="gemini-2.5-flash", contents=prompt + model="gemini-3.5-flash", contents=prompt ) # Prompt and response tokens count diff --git a/genai/count_tokens/counttoken_with_txt.py b/genai/count_tokens/counttoken_with_txt.py index 84464c5cf82..7eb37a8d2ab 100644 --- a/genai/count_tokens/counttoken_with_txt.py +++ b/genai/count_tokens/counttoken_with_txt.py @@ -20,12 +20,12 @@ def count_tokens() -> int: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.count_tokens( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="What's the highest mountain in Africa?", ) print(response) # Example output: - # total_tokens=10 + # total_tokens=9 # cached_content_token_count=None # [END googlegenaisdk_counttoken_with_txt] return response.total_tokens diff --git a/genai/count_tokens/counttoken_with_txt_vid.py b/genai/count_tokens/counttoken_with_txt_vid.py index e32f14f0845..e8d1ec72f67 100644 --- a/genai/count_tokens/counttoken_with_txt_vid.py +++ b/genai/count_tokens/counttoken_with_txt_vid.py @@ -29,7 +29,7 @@ def count_tokens() -> int: ] response = client.models.count_tokens( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=contents, ) print(response) diff --git a/genai/count_tokens/noxfile_config.py b/genai/count_tokens/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/count_tokens/noxfile_config.py +++ b/genai/count_tokens/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/count_tokens/requirements-test.txt b/genai/count_tokens/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/genai/count_tokens/requirements-test.txt +++ b/genai/count_tokens/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/genai/count_tokens/requirements.txt b/genai/count_tokens/requirements.txt index 7ad844cebcf..726dd09178a 100644 --- a/genai/count_tokens/requirements.txt +++ b/genai/count_tokens/requirements.txt @@ -1 +1,2 @@ -google-genai==1.27.0 +google-genai==1.42.0 +sentencepiece==0.2.1 diff --git a/genai/count_tokens/test_count_tokens_examples.py b/genai/count_tokens/test_count_tokens_examples.py index b654ff872d8..94f5b06c036 100644 --- a/genai/count_tokens/test_count_tokens_examples.py +++ b/genai/count_tokens/test_count_tokens_examples.py @@ -19,11 +19,13 @@ import os import counttoken_compute_with_txt +import counttoken_localtokenizer_compute_with_txt +import counttoken_localtokenizer_with_txt import counttoken_resp_with_txt import counttoken_with_txt import counttoken_with_txt_vid -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "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" @@ -43,3 +45,11 @@ def test_counttoken_with_txt() -> None: def test_counttoken_with_txt_vid() -> None: assert counttoken_with_txt_vid.count_tokens() + + +def test_counttoken_localtokenizer_with_txt() -> None: + assert counttoken_localtokenizer_with_txt.counttoken_localtokenizer_with_txt() + + +def test_counttoken_localtokenizer_compute_with_txt() -> None: + assert counttoken_localtokenizer_compute_with_txt.counttoken_localtokenizer_compute_with_txt() diff --git a/genai/embeddings/code_retrieval_example.py b/genai/embeddings/code_retrieval_example.py new file mode 100644 index 00000000000..904b1bbaccd --- /dev/null +++ b/genai/embeddings/code_retrieval_example.py @@ -0,0 +1,77 @@ +# 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. + +# [START aiplatform_genai_embedding_code_retrieval] +import os + +from google import genai + +# TODO (Developer) set the following environment variables. +PROJECT_ID = os.getenv("PROJECT_ID") +LOCATION_ID = os.getenv("LOCATION_ID", "us-central1") +MODEL_NAME = os.getenv("MODEL_NAME", "gemini-embedding-001") + +QUERY_LINES = ["Retrieve a function that adds two numbers"] +CODE_RETRIEVAL_QUERY = "CODE_RETRIEVAL_QUERY" +RETRIEVAL_DOCUMENT = "RETRIEVAL_DOCUMENT" +SOURCE_CODE = [ + "def func(a, b): return a + b", + "def func(a, b): return a - b", + "def func(a, b): return (a ** 2 + b ** 2) ** 0.5", +] + + +def embed_test() -> ( + tuple[genai.types.EmbedContentResponse, genai.types.EmbedContentResponse] +): + """Generates embeddings for source code indexing and code search queries using the Gemini API. + + Returns: + tuple[genai.types.EmbedContentResponse, genai.types.EmbedContentResponse]: A tuple containing + the final source code indexing response and search query embedding response. + """ + client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION_ID) + + # Index Source Code + for line in SOURCE_CODE: + config = genai.types.EmbedContentConfig(task_type=RETRIEVAL_DOCUMENT) + + index_response = client.models.embed_content( + model=MODEL_NAME, contents=line, config=config + ) + + print( + f"Task: {RETRIEVAL_DOCUMENT} | " + f"Vector length: {len(index_response.embeddings)} | " + f"Preview: {index_response.embeddings[:3]}..." + ) + + # Embed Search Prompts + for line in QUERY_LINES: + config = genai.types.EmbedContentConfig(task_type=CODE_RETRIEVAL_QUERY) + + query_response = client.models.embed_content( + model=MODEL_NAME, contents=line, config=config + ) + + print( + f"Task: {CODE_RETRIEVAL_QUERY} | " + f"Vector length: {len(query_response.embeddings)} | " + f"Preview: {query_response.embeddings[:3]}..." + ) + + return index_response, query_response + + +# [END aiplatform_genai_embedding_code_retrieval] diff --git a/genai/embeddings/generate_embeddings_with_lower_dimension.py b/genai/embeddings/generate_embeddings_with_lower_dimension.py new file mode 100644 index 00000000000..27feb6a5551 --- /dev/null +++ b/genai/embeddings/generate_embeddings_with_lower_dimension.py @@ -0,0 +1,71 @@ +# 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. + +# [START aiplatform_genai_embeddings_specify_lower_dimension] +import os + +from google import genai + +# TODO (Developer) Set environment variables +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +LOCATION_ID = "global" + +# Supported dimensions: 128, 256, 512, 1408 (or up to 3072 for gemini-embedding-2) +EMBEDDING_DIMENSION = 128 +IMAGE_URI = "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png" +EMBEDDING_MODEL = "gemini-embedding-2" +CONTEXTUAL_TEXT = "Colosseum" + + +def generate_embeddings_with_lower_dimension() -> genai.types.EmbedContentResponse: + """Generates multimodal embeddings (image + text) with custom lower dimensionality + + using the modern google-genai SDK. + """ + + client = genai.Client( + vertexai=True, + project=PROJECT_ID, + location=LOCATION_ID, + ) + + image_part = genai.types.Part.from_uri( + file_uri=IMAGE_URI, + mime_type="image/png", + ) + + text_part = genai.types.Part.from_text(text=CONTEXTUAL_TEXT) + + contents = genai.types.Content(parts=[image_part, text_part]) + + config = genai.types.EmbedContentConfig(output_dimensionality=EMBEDDING_DIMENSION) + + response = client.models.embed_content( + model=EMBEDDING_MODEL, + contents=[contents], + config=config, + ) + + if response.embeddings: + + embeddings = response.embeddings[0].values + + print(f"Embeddings (dim={len(embeddings)}): {embeddings[:3]}...\n") + + print(response) + + return response + + +# [END aiplatform_genai_embeddings_specify_lower_dimension] diff --git a/genai/embeddings/model_tuning_example.py b/genai/embeddings/model_tuning_example.py new file mode 100644 index 00000000000..c6d7c268ef2 --- /dev/null +++ b/genai/embeddings/model_tuning_example.py @@ -0,0 +1,78 @@ +# 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. + +# [START aiplatform_genai_embedding_model_tuning] +import os + +from google.cloud import aiplatform + +# TODO (Developer) set the following environment variables. +PROJECT_ID = os.getenv("PROJECT_ID") +LOCATION_ID = os.getenv("LOCATION_ID", "us-central1") +MODEL_NAME = os.getenv("MODEL_NAME", "text-embedding-004") +# A storage bucket: gs://your-bucket-name/embedding-tuning-output +OUTPUT_URI = os.getenv("OUTPUT_DIR") + +TRAIN_LABEL_PATH = ( + "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/train.tsv" +) +TEST_LABEL_PATH = ( + "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/test.tsv" +) +CORPUS_PATH = ( + "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/corpus.jsonl" +) +QUERIES_PATH = ( + "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/queries.jsonl" +) + +ACCELERATOR_TYPE = "NVIDIA_L4" + +# Official Google Cloud KFP pipeline template URI for text embedding model tuning +EMBEDDING_TUNING_PIPELINE_URI = "https://us-kfp.pkg.dev/ml-pipeline/llm-text-embedding/tune-text-embedding-model/v1.1.3" + + +def tune_embedding_model() -> aiplatform.PipelineJob: + """Tune an embedding model using the specified parameters.""" + + aiplatform.init(project=PROJECT_ID, location=LOCATION_ID) + + # Configure parameters expected by the embedding tuning pipeline template + pipeline_parameters = { + "base_model_version_id": MODEL_NAME, + "corpus_path": CORPUS_PATH, + "queries_path": QUERIES_PATH, + "train_label_path": TRAIN_LABEL_PATH, + "test_label_path": TEST_LABEL_PATH, + "accelerator_type": ACCELERATOR_TYPE, + } + + # Instantiate the Vertex AI Pipeline job + pipeline_job = aiplatform.PipelineJob( + display_name="tune-text-embedding-model-job", + template_path=EMBEDDING_TUNING_PIPELINE_URI, + pipeline_root=OUTPUT_URI, + parameter_values=pipeline_parameters, + project=PROJECT_ID, + location=LOCATION_ID, + ) + + pipeline_job.submit() + + print(f"Pipeline submitted successfully: {pipeline_job.resource_name}") + + return pipeline_job + + +# [END aiplatform_genai_embedding_model_tuning] diff --git a/genai/embeddings/multimodal_embedding_image.py b/genai/embeddings/multimodal_embedding_image.py new file mode 100644 index 00000000000..5444f768bc4 --- /dev/null +++ b/genai/embeddings/multimodal_embedding_image.py @@ -0,0 +1,47 @@ +# 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. + +# [START aiplatform_genai_multimodal_embedding_image] +from google import genai +from google.genai import types + + +def embed_content() -> types.EmbedContentResponse: + client = genai.Client() + + content = types.Content( + parts=[ + types.Part.from_uri( + file_uri="gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png", + mime_type="image/png", + ), + types.Part.from_text(text="Colosseum"), + ], + ) + + response = client.models.embed_content( + model="gemini-embedding-2", + contents=[content], + config=types.EmbedContentConfig( + output_dimensionality=1408, + ), + ) + print(response) + # Example response: + # embeddings=[ContentEmbedding(values=[-0.0123147098, 0.0727171078, ...])] + return response + + +# [END aiplatform_genai_multimodal_embedding_image] + diff --git a/genai/embeddings/multimodal_embedding_video.py b/genai/embeddings/multimodal_embedding_video.py new file mode 100644 index 00000000000..27ecf722186 --- /dev/null +++ b/genai/embeddings/multimodal_embedding_video.py @@ -0,0 +1,43 @@ +# 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. + +# [START aiplatform_genai_multimodal_embedding_video] +from google import genai +from google.genai import types + + +def embed_content() -> types.EmbedContentResponse: + client = genai.Client() + + part = types.Part( + file_data=types.FileData( + file_uri="gs://cloud-samples-data/vertex-ai-vision/highway_vehicles.mp4", + mime_type="video/mp4", + ), + video_metadata=types.VideoMetadata(end_offset="1s"), + ) + + content = types.Content(parts=[part]) + + response = client.models.embed_content( + model="gemini-embedding-2", + contents=[content], + ) + print(response) + # Example response: + # embeddings=[ContentEmbedding(values=[-0.0123147098, 0.0727171078, ...])] + return response + + +# [END aiplatform_genai_multimodal_embedding_video] diff --git a/genai/embeddings/multimodal_example.py b/genai/embeddings/multimodal_example.py new file mode 100644 index 00000000000..a6c76e0e2af --- /dev/null +++ b/genai/embeddings/multimodal_example.py @@ -0,0 +1,68 @@ +# 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. + +# [START aiplatform_genai_multimodal_embedding_image_video_text] + +import os + +from google import genai + +# Environment configuration +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "your-project-id") +LOCATION_ID = "global" + +EMBEDDING_MODEL = "gemini-embedding-2" +IMAGE_URI = "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png" +VIDEO_URI = "gs://cloud-samples-data/vertex-ai-vision/highway_vehicles.mp4" +CONTEXTUAL_TEXT = "Cars on Highway" + + +def get_image_video_text_embeddings() -> genai.types.EmbedContentResponse: + """Generates multimodal embeddings from image, video, and text using the google-genai SDK.""" + + client = genai.Client( + vertexai=True, + project=PROJECT_ID, + location=LOCATION_ID, + ) + + image_part = genai.types.Part.from_uri( + file_uri=IMAGE_URI, + mime_type="image/png", + ) + + video_part = genai.types.Part.from_uri( + file_uri=VIDEO_URI, + mime_type="video/mp4", + ) + + content = genai.types.Content( + parts=[image_part, video_part, genai.types.Part.from_text(text=CONTEXTUAL_TEXT)] + ) + + # Joint/Interleaved Multimodal Embedding (Image + Video + Text in same vector space) + response = client.models.embed_content(model=EMBEDDING_MODEL, contents=content) + + if response.embeddings: + + vector = response.embeddings[0].values + + print(f"Embeddings ({len(vector)} dims): {vector[:3]}...") + + print(response) + + return response + + +# [END aiplatform_genai_multimodal_embedding_image_video_text] diff --git a/genai/embeddings/noxfile_config.py b/genai/embeddings/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/embeddings/noxfile_config.py +++ b/genai/embeddings/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/embeddings/requirements-test.txt b/genai/embeddings/requirements-test.txt index e43b7792721..966a12401f2 100644 --- a/genai/embeddings/requirements-test.txt +++ b/genai/embeddings/requirements-test.txt @@ -1,2 +1,2 @@ -google-api-core==2.24.0 -pytest==8.2.0 +google-api-core==2.33.0 +pytest==9.1.1 diff --git a/genai/embeddings/requirements.txt b/genai/embeddings/requirements.txt index 7ad844cebcf..88bf66e0d7d 100644 --- a/genai/embeddings/requirements.txt +++ b/genai/embeddings/requirements.txt @@ -1 +1,2 @@ -google-genai==1.27.0 +google-genai==2.16.0 +google-cloud-aiplatform[pipelines]==1.163.0 diff --git a/genai/embeddings/test_embeddings_examples.py b/genai/embeddings/test_embeddings_examples.py index 5908ccddc6a..7f1f988efb0 100644 --- a/genai/embeddings/test_embeddings_examples.py +++ b/genai/embeddings/test_embeddings_examples.py @@ -18,10 +18,17 @@ import os +import code_retrieval_example import embeddings_docretrieval_with_txt +import generate_embeddings_with_lower_dimension +import model_tuning_example +import multimodal_embedding_image +import multimodal_embedding_video +import multimodal_example -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" -os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" + +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" +os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" @@ -29,3 +36,33 @@ def test_embeddings_docretrieval_with_txt() -> None: response = embeddings_docretrieval_with_txt.embed_content() assert response + + +def test_code_retrieval_example() -> None: + response = code_retrieval_example.embed_test() + assert response + + +def test_model_tuning_example() -> None: + response = model_tuning_example.tune_embedding_model() + assert response + + +def test_multimodal_example() -> None: + response = multimodal_example.get_image_video_text_embeddings() + assert response + + +def test_generate_embeddings_with_lower_dimension() -> None: + response = generate_embeddings_with_lower_dimension.generate_embeddings_with_lower_dimension() + assert response + + +def test_multimodal_embedding_image() -> None: + response = multimodal_embedding_image.embed_content() + assert response + + +def test_multimodal_embedding_video() -> None: + response = multimodal_embedding_video.embed_content() + assert response diff --git a/appengine/flexible_python37_and_earlier/storage/noxfile_config.py b/genai/evaluation/noxfile_config.py similarity index 87% rename from appengine/flexible_python37_and_earlier/storage/noxfile_config.py rename to genai/evaluation/noxfile_config.py index 6c2c81fa22b..610630abf66 100644 --- a/appengine/flexible_python37_and_earlier/storage/noxfile_config.py +++ b/genai/evaluation/noxfile_config.py @@ -1,4 +1,4 @@ -# Copyright 2021 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,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": ["3.8", "3.9", "3.10", "3.12", "3.13"], # 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/genai/evaluation/pairwise_summarization_quality.py b/genai/evaluation/pairwise_summarization_quality.py new file mode 100644 index 00000000000..85ea7efd607 --- /dev/null +++ b/genai/evaluation/pairwise_summarization_quality.py @@ -0,0 +1,119 @@ +# 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. + +# [START aiplatform_genai_evaluation_pairwise_summarization_quality] + +import os + +from google import genai +from google.genai import types + +import pandas as pd + +from vertexai.evaluation import ( + EvalTask, + MetricPromptTemplateExamples, + PairwiseMetric, +) +from vertexai.preview.evaluation import EvalResult + +# TODO (developer) set GOOGLE_CLOUD_PROJECT and REGION_ID +# environment variables before running. +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +LOCATION = os.getenv("REGION_ID") +BASELINE_MODEL = os.getenv("BASELINE_MODEL", "gemini-2.5-flash") +CANDIDATE_MODEL = os.getenv("CANDIDATE_MODEL", "gemini-2.5-pro") + +PROMPT = """ + Summarize the text such that a five-year-old can understand. + + # Text + + As part of a comprehensive initiative to tackle urban congestion and foster + sustainable urban living, a major city has revealed ambitious plans for an + extensive overhaul of its public transportation system. The project aims not + only to improve the efficiency and reliability of public transit but also to + reduce the city\'s carbon footprint and promote eco-friendly commuting options. + City officials anticipate that this strategic investment will enhance + accessibility for residents and visitors alike, ushering in a new era of + efficient, environmentally conscious urban transportation. + """ + + +def evaluate_output() -> EvalResult: + """ + Evaluates a candidate model's summarization quality + against a baseline model using Vertex AI. + """ + + baseline_responses = [] + candidate_responses = [] + + genai_client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION) + + baseline_resp = genai_client.models.generate_content( + model=BASELINE_MODEL, + contents=PROMPT, + config=types.GenerateContentConfig(temperature=0.4), + ) + baseline_responses.append(baseline_resp.text) + + candidate_resp = genai_client.models.generate_content( + model=CANDIDATE_MODEL, + contents=PROMPT, + config=types.GenerateContentConfig(temperature=0.4), + ) + candidate_responses.append(candidate_resp.text) + + eval_df = pd.DataFrame( + { + "prompt": PROMPT, + "response": candidate_responses, + "baseline_model_response": baseline_responses, + } + ) + + prompt_template = MetricPromptTemplateExamples.get_prompt_template( + "pairwise_summarization_quality" + ) + + pairwise_text_quality = PairwiseMetric( + metric="pairwise_summarization_quality", + metric_prompt_template=prompt_template, + ) + + eval_task = EvalTask( + dataset=eval_df, + metrics=[pairwise_text_quality], + experiment="pairwise-benchmark2", + ) + + comparison_result = eval_task.evaluate() + + pd.set_option("display.max_columns", None) + pd.set_option("display.max_colwidth", 250) + + columns_to_print = [ + "prompt", + "baseline_model_response", + "response", + "pairwise_summarization_quality/pairwise_choice", + "pairwise_summarization_quality/explanation", + ] + print(comparison_result.metrics_table[columns_to_print]) + + return comparison_result + + +# [END aiplatform_genai_evaluation_pairwise_summarization_quality] diff --git a/genai/evaluation/requirements-test.txt b/genai/evaluation/requirements-test.txt new file mode 100644 index 00000000000..b3b2b3ca4cc --- /dev/null +++ b/genai/evaluation/requirements-test.txt @@ -0,0 +1 @@ +pytest==9.0.3 diff --git a/genai/evaluation/requirements.txt b/genai/evaluation/requirements.txt new file mode 100644 index 00000000000..f039d20dab4 --- /dev/null +++ b/genai/evaluation/requirements.txt @@ -0,0 +1,4 @@ +pandas==2.3.3 +google-auth==2.55.2 +google-cloud-aiplatform[evaluation]==1.161.0 +google-genai==2.12.1 diff --git a/generative_ai/provisioned_throughput/test_provisioned_throughput_examples.py b/genai/evaluation/test_evaluation.py similarity index 74% rename from generative_ai/provisioned_throughput/test_provisioned_throughput_examples.py rename to genai/evaluation/test_evaluation.py index 89f7c38df62..fbf1fee0562 100644 --- a/generative_ai/provisioned_throughput/test_provisioned_throughput_examples.py +++ b/genai/evaluation/test_evaluation.py @@ -1,4 +1,4 @@ -# Copyright 2025 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. @@ -12,10 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pairwise_summarization_quality -import provisioned_throughput_with_txt - - -def test_provisioned_throughput_with_txt() -> None: - response = provisioned_throughput_with_txt.generate_content() +def test_pairwise_evaluation_summarization_quality() -> None: + response = pairwise_summarization_quality.evaluate_output() assert response diff --git a/genai/express_mode/api_key_example.py b/genai/express_mode/api_key_example.py index 21f8ab0e81d..47f390b6914 100644 --- a/genai/express_mode/api_key_example.py +++ b/genai/express_mode/api_key_example.py @@ -23,7 +23,7 @@ def generate_content() -> str: client = genai.Client(vertexai=True, api_key=API_KEY) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="Explain bubble sort to me.", ) diff --git a/genai/express_mode/noxfile_config.py b/genai/express_mode/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/express_mode/noxfile_config.py +++ b/genai/express_mode/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/express_mode/requirements-test.txt b/genai/express_mode/requirements-test.txt index e43b7792721..22a9617b8e8 100644 --- a/genai/express_mode/requirements-test.txt +++ b/genai/express_mode/requirements-test.txt @@ -1,2 +1,2 @@ google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/express_mode/requirements.txt b/genai/express_mode/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/express_mode/requirements.txt +++ b/genai/express_mode/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/express_mode/test_express_mode_examples.py b/genai/express_mode/test_express_mode_examples.py index 7b2ff26511a..388965f06cd 100644 --- a/genai/express_mode/test_express_mode_examples.py +++ b/genai/express_mode/test_express_mode_examples.py @@ -40,7 +40,7 @@ def test_api_key_example(mock_genai_client: MagicMock) -> None: mock_genai_client.assert_called_once_with(vertexai=True, api_key="YOUR_API_KEY") mock_genai_client.return_value.models.generate_content.assert_called_once_with( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="Explain bubble sort to me.", ) assert response == "This is a mocked bubble sort explanation." 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 8f7a00e733c..405702d4eff 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-preview", + model="gemini-3.1-flash-image", 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_img_with_vid.py b/genai/image_generation/imggen_mmflash_img_with_vid.py new file mode 100644 index 00000000000..98a0c51f996 --- /dev/null +++ b/genai/image_generation/imggen_mmflash_img_with_vid.py @@ -0,0 +1,51 @@ +# 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() -> str: + # [START googlegenaisdk_imggen_mmflash_img_with_vid] + from google import genai + from google.genai.types import GenerateContentConfig, Modality, Part + from PIL import Image + from io import BytesIO + + client = genai.Client() + + # A video on 'The ABCs of agent building' + video = "https://www.youtube.com/watch?v=rjoMZyxncUI" + + response = client.models.generate_content( + model="gemini-3.1-flash-image", + contents=[ + Part.from_uri( + file_uri=video, + mime_type="video/mp4" + ), + "Generate an infographic of the topics covered in this video." + ], + config=GenerateContentConfig(response_modalities=[Modality.TEXT, Modality.IMAGE]), + ) + for part in response.candidates[0].content.parts: + if part.text: + print(part.text) + elif part.inline_data: + image = Image.open(BytesIO((part.inline_data.data))) + image.save("output_folder/video-image.png") + + # [END googlegenaisdk_imggen_mmflash_img_with_vid] + return "output_folder/video-image.png" + + +if __name__ == "__main__": + generate_content() diff --git a/genai/image_generation/imggen_mmflash_locale_aware_with_txt.py b/genai/image_generation/imggen_mmflash_locale_aware_with_txt.py index 9114087babc..2e80866a525 100644 --- a/genai/image_generation/imggen_mmflash_locale_aware_with_txt.py +++ b/genai/image_generation/imggen_mmflash_locale_aware_with_txt.py @@ -23,7 +23,7 @@ def generate_content() -> str: client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-flash-image-preview", + model="gemini-3.1-flash-image", contents=("Generate a photo of a breakfast meal."), config=GenerateContentConfig(response_modalities=[Modality.TEXT, Modality.IMAGE]), ) diff --git a/genai/image_generation/imggen_mmflash_multiple_imgs_with_txt.py b/genai/image_generation/imggen_mmflash_multiple_imgs_with_txt.py index 51326b5b738..884dba9d45f 100644 --- a/genai/image_generation/imggen_mmflash_multiple_imgs_with_txt.py +++ b/genai/image_generation/imggen_mmflash_multiple_imgs_with_txt.py @@ -23,7 +23,7 @@ def generate_content() -> str: client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-flash-image-preview", + model="gemini-3.1-flash-image", contents=("Generate 3 images a cat sitting on a chair."), config=GenerateContentConfig(response_modalities=[Modality.TEXT, Modality.IMAGE]), ) 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 057315157c2..ff2b9fda9f9 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-preview", + model="gemini-3.1-flash-image", 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 41aca349d6d..fd83f89eeda 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-preview", + model="gemini-3.1-flash-image", 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_upscale_with_img.py b/genai/image_generation/imggen_upscale_with_img.py new file mode 100644 index 00000000000..c3ea9ffa640 --- /dev/null +++ b/genai/image_generation/imggen_upscale_with_img.py @@ -0,0 +1,45 @@ +# 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. + +from google.genai.types import Image + + +def upscale_images(output_file: str) -> Image: + # [START googlegenaisdk_imggen_upscale_with_img] + from google import genai + from google.genai.types import Image + + client = genai.Client() + + # TODO(developer): Update and un-comment below line + # output_file = "output-image.png" + + image = client.models.upscale_image( + model="imagen-4.0-upscale-preview", + image=Image.from_file(location="test_resources/dog_newspaper.png"), + upscale_factor="x2", + ) + + image.generated_images[0].image.save(output_file) + + print(f"Created output image using {len(image.generated_images[0].image.image_bytes)} bytes") + # Example response: + # Created output image using 1234567 bytes + + # [END googlegenaisdk_imggen_upscale_with_img] + return image.generated_images[0].image + + +if __name__ == "__main__": + upscale_images(output_file="output_folder/dog_newspaper.png") 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/image_generation/noxfile_config.py b/genai/image_generation/noxfile_config.py index d63baa25bfa..0973c8621c7 100644 --- a/genai/image_generation/noxfile_config.py +++ b/genai/image_generation/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.9", "3.10", "3.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/image_generation/output_folder/bw-example-image.png b/genai/image_generation/output_folder/bw-example-image.png index fe252e54bce..5c2289f477c 100644 Binary files a/genai/image_generation/output_folder/bw-example-image.png and b/genai/image_generation/output_folder/bw-example-image.png differ diff --git a/genai/image_generation/output_folder/example-cats-01.png b/genai/image_generation/output_folder/example-cats-01.png index 21c27c7f145..6ec55171571 100644 Binary files a/genai/image_generation/output_folder/example-cats-01.png and b/genai/image_generation/output_folder/example-cats-01.png differ diff --git a/genai/image_generation/output_folder/example-image-eiffel-tower.png b/genai/image_generation/output_folder/example-image-eiffel-tower.png index 276aad1148b..0cf9b0e50de 100644 Binary files a/genai/image_generation/output_folder/example-image-eiffel-tower.png and b/genai/image_generation/output_folder/example-image-eiffel-tower.png differ diff --git a/genai/image_generation/requirements-test.txt b/genai/image_generation/requirements-test.txt index 4ccc4347cbe..14a8493498b 100644 --- a/genai/image_generation/requirements-test.txt +++ b/genai/image_generation/requirements-test.txt @@ -1,3 +1,3 @@ google-api-core==2.24.0 google-cloud-storage==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/image_generation/requirements.txt b/genai/image_generation/requirements.txt index 38b43a4cb10..75a39f7e582 100644 --- a/genai/image_generation/requirements.txt +++ b/genai/image_generation/requirements.txt @@ -1,2 +1,2 @@ -google-genai==1.29.0 -pillow==11.1.0 +google-genai==1.42.0 +pillow==12.3.0; python_version >= "3.10" diff --git a/genai/image_generation/test_image_generation.py b/genai/image_generation/test_image_generation.py index dd037ba6dbf..f5cf14bb62c 100644 --- a/genai/image_generation/test_image_generation.py +++ b/genai/image_generation/test_image_generation.py @@ -37,10 +37,11 @@ import imggen_scribble_ctrl_type_with_txt_img import imggen_style_reference_with_txt_img import imggen_subj_refer_ctrl_refer_with_txt_imgs +import imggen_upscale_with_img import imggen_virtual_try_on_with_txt_img import imggen_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" @@ -147,3 +148,9 @@ def test_img_virtual_try_on() -> None: OUTPUT_FILE = os.path.join(RESOURCES, "man_in_sweater.png") response = imggen_virtual_try_on_with_txt_img.virtual_try_on(OUTPUT_FILE) assert response + + +def test_img_upscale() -> None: + OUTPUT_FILE = os.path.join(RESOURCES, "dog_newspaper.png") + response = imggen_upscale_with_img.upscale_images(OUTPUT_FILE) + assert response diff --git a/genai/image_generation/test_image_generation_mmflash.py b/genai/image_generation/test_image_generation_mmflash.py index 3ae60ec66ba..1b6b46bae70 100644 --- a/genai/image_generation/test_image_generation_mmflash.py +++ b/genai/image_generation/test_image_generation_mmflash.py @@ -19,13 +19,14 @@ import os import imggen_mmflash_edit_img_with_txt_img +import imggen_mmflash_img_with_vid import imggen_mmflash_locale_aware_with_txt import imggen_mmflash_multiple_imgs_with_txt import imggen_mmflash_txt_and_img_with_txt import imggen_mmflash_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" @@ -49,3 +50,6 @@ def test_imggen_mmflash_locale_aware_with_txt() -> None: def test_imggen_mmflash_multiple_imgs_with_txt() -> None: assert imggen_mmflash_multiple_imgs_with_txt.generate_content() + +def test_imggen_mmflash_img_with_vid() -> None: + assert imggen_mmflash_img_with_vid.generate_content() diff --git a/genai/image_generation/test_resources/dog_newspaper.png b/genai/image_generation/test_resources/dog_newspaper.png index 0e502cdbb61..5f8961e6c10 100644 Binary files a/genai/image_generation/test_resources/dog_newspaper.png and b/genai/image_generation/test_resources/dog_newspaper.png differ diff --git a/genai/image_generation/test_resources/latte_edit.png b/genai/image_generation/test_resources/latte_edit.png index ec8f61ef661..f5f7465c36f 100644 Binary files a/genai/image_generation/test_resources/latte_edit.png and b/genai/image_generation/test_resources/latte_edit.png differ diff --git a/genai/image_generation/test_resources/living_room_edit.png b/genai/image_generation/test_resources/living_room_edit.png index 635498dc1e2..c949440e101 100644 Binary files a/genai/image_generation/test_resources/living_room_edit.png and b/genai/image_generation/test_resources/living_room_edit.png differ diff --git a/genai/labels/labels_example.py b/genai/labels/labels_example.py new file mode 100644 index 00000000000..02e22857933 --- /dev/null +++ b/genai/labels/labels_example.py @@ -0,0 +1,46 @@ +# 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. + +# [START aiplatform_genai_gemini_set_labels] + +import os + +from google import genai + +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +LOCATION = os.getenv("LOCATION_ID") +GEMINI_MODEL = os.getenv("GEMINI_MODEL_ID", "gemini-2.5-flash") + + +def generate_content() -> genai.types.GenerateContentResponse: + + genai_client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION) + + labels = { + "environment": "testing", + "feature": "genai", + "model": "gemini", + } + + config = genai.types.GenerateContentConfig(temperature=0.4, labels=labels) + + response = genai_client.models.generate_content( + model=GEMINI_MODEL, contents="What is Generative AI?", config=config + ) + + print(response.text) + return response + + +# [END aiplatform_genai_gemini_set_labels] diff --git a/appengine/flexible_python37_and_earlier/datastore/noxfile_config.py b/genai/labels/noxfile_config.py similarity index 82% rename from appengine/flexible_python37_and_earlier/datastore/noxfile_config.py rename to genai/labels/noxfile_config.py index 1665dd736f8..650b3c47840 100644 --- a/appengine/flexible_python37_and_earlier/datastore/noxfile_config.py +++ b/genai/labels/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": ["3.8", "3.9", "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, + "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/labels/requirements-test.txt b/genai/labels/requirements-test.txt new file mode 100644 index 00000000000..b47809aab43 --- /dev/null +++ b/genai/labels/requirements-test.txt @@ -0,0 +1,2 @@ +google-api-core==2.32.0 +pytest==9.0.3 diff --git a/genai/labels/requirements.txt b/genai/labels/requirements.txt new file mode 100644 index 00000000000..b523bb9b53d --- /dev/null +++ b/genai/labels/requirements.txt @@ -0,0 +1 @@ +google-genai==2.12.1 diff --git a/generative_ai/labels/test_labels_examples.py b/genai/labels/test_labels_examples.py similarity index 92% rename from generative_ai/labels/test_labels_examples.py rename to genai/labels/test_labels_examples.py index 52a21689861..63d1f108587 100644 --- a/generative_ai/labels/test_labels_examples.py +++ b/genai/labels/test_labels_examples.py @@ -1,4 +1,4 @@ -# Copyright 2024 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. @@ -17,4 +17,4 @@ def test_set_labels() -> None: response = labels_example.generate_content() - assert response + assert response.text diff --git a/genai/live/live_audio_with_txt.py b/genai/live/live_audio_with_txt.py new file mode 100644 index 00000000000..3860b9f0128 --- /dev/null +++ b/genai/live/live_audio_with_txt.py @@ -0,0 +1,85 @@ +# 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. + +# Test file: https://storage.googleapis.com/generativeai-downloads/data/16000.wav +# Install helpers for converting files: pip install librosa soundfile simpleaudio + +import asyncio + + +async def generate_content() -> list: + # [START googlegenaisdk_live_audio_with_txt] + from google import genai + from google.genai.types import ( + Content, LiveConnectConfig, Modality, Part, + PrebuiltVoiceConfig, SpeechConfig, VoiceConfig + ) + import numpy as np + import soundfile as sf + import simpleaudio as sa + + def play_audio(audio_array: np.ndarray, sample_rate: int = 24000) -> None: + sf.write("output.wav", audio_array, sample_rate) + wave_obj = sa.WaveObject.from_wave_file("output.wav") + play_obj = wave_obj.play() + play_obj.wait_done() + + client = genai.Client() + voice_name = "Aoede" + model = "gemini-live-2.5-flash-native-audio" + + config = LiveConnectConfig( + response_modalities=[Modality.AUDIO], + speech_config=SpeechConfig( + voice_config=VoiceConfig( + prebuilt_voice_config=PrebuiltVoiceConfig( + voice_name=voice_name, + ) + ), + ), + ) + + async with client.aio.live.connect( + model=model, + config=config, + ) as session: + text_input = "Hello? Gemini are you there?" + print("> ", text_input, "\n") + + await session.send_client_content( + turns=Content(role="user", parts=[Part(text=text_input)]) + ) + + audio_data = [] + async for message in session.receive(): + if ( + message.server_content.model_turn + and message.server_content.model_turn.parts + ): + for part in message.server_content.model_turn.parts: + if part.inline_data: + audio_data.append( + np.frombuffer(part.inline_data.data, dtype=np.int16) + ) + + if audio_data: + print("Received audio answer: ") + play_audio(np.concatenate(audio_data), sample_rate=24000) + + # [END googlegenaisdk_live_audio_with_txt] + return [] + + +if __name__ == "__main__": + asyncio.run(generate_content()) diff --git a/genai/live/live_audiogen_with_txt.py b/genai/live/live_audiogen_with_txt.py index cf7f24a6fc4..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" @@ -82,7 +82,7 @@ async def generate_content() -> None: # Received audio answer. Saving to local file... # Audio saved to gemini_response.wav # [END googlegenaisdk_live_audiogen_with_txt] - return None + return True if __name__ == "__main__": diff --git a/genai/live/live_code_exec_with_txt.py b/genai/live/live_code_exec_with_txt.py index 70db7402ee7..aab142bceae 100644 --- a/genai/live/live_code_exec_with_txt.py +++ b/genai/live/live_code_exec_with_txt.py @@ -18,11 +18,17 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_code_exec_with_txt] from google import genai - from google.genai.types import (Content, LiveConnectConfig, Modality, Part, - Tool, ToolCodeExecution) + from google.genai.types import ( + Content, + LiveConnectConfig, + Modality, + Part, + Tool, + ToolCodeExecution, + ) client = genai.Client() - model_id = "gemini-2.0-flash-live-preview-04-09" + model_id = "gemini-live-2.5-flash-native-audio" config = LiveConnectConfig( response_modalities=[Modality.TEXT], tools=[Tool(code_execution=ToolCodeExecution())], @@ -55,7 +61,7 @@ async def generate_content() -> list[str]: # > Compute the largest prime palindrome under 10 # Final Answer: The final answer is $\boxed{7}$ # [END googlegenaisdk_live_code_exec_with_txt] - return response + return True if __name__ == "__main__": diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py new file mode 100644 index 00000000000..5d5b5a05445 --- /dev/null +++ b/genai/live/live_conversation_audio_with_audio.py @@ -0,0 +1,133 @@ +# 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. + +# [START googlegenaisdk_live_conversation_audio_with_audio] + +import asyncio +import base64 + +from google import genai +from google.genai.types import ( + AudioTranscriptionConfig, + Blob, + HttpOptions, + LiveConnectConfig, + Modality, +) +import numpy as np + +from scipy.io import wavfile + +# The number of audio frames to send in each chunk. +CHUNK = 4200 +CHANNELS = 1 +MODEL = "gemini-live-2.5-flash-native-audio" + +# The audio sample rate expected by the model. +INPUT_RATE = 16000 +# The audio sample rate of the audio generated by the model. +OUTPUT_RATE = 24000 + +# The sample width for 16-bit audio, which is standard for this type of audio data. +SAMPLE_WIDTH = 2 + +client = genai.Client(http_options=HttpOptions(api_version="v1beta1"), location="us-central1") + + +def read_wavefile(filepath: str) -> tuple[str, str]: + # Read the .wav file using scipy.io.wavfile.read + rate, data = wavfile.read(filepath) + # Convert the NumPy array of audio samples back to raw bytes + raw_audio_bytes = data.tobytes() + # Encode the raw bytes to a base64 string. + # The result needs to be decoded from bytes to a UTF-8 string + base64_encoded_data = base64.b64encode(raw_audio_bytes).decode("ascii") + mime_type = f"audio/pcm;rate={rate}" + return base64_encoded_data, mime_type + + +def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int) -> None: + """Writes a list of audio byte frames to a WAV file using scipy.""" + # Combine the list of byte frames into a single byte string + raw_audio_bytes = b"".join(audio_frames) + + # Convert the raw bytes to a NumPy array. + # The sample width is 2 bytes (16-bit), so we use np.int16 + audio_data = np.frombuffer(raw_audio_bytes, dtype=np.int16) + + # Write the NumPy array to a .wav file + wavfile.write(filepath, rate, audio_data) + print(f"Model response saved to {filepath}") + + +async def main() -> bool: + print("Starting the code") + + async with client.aio.live.connect( + model=MODEL, + config=LiveConnectConfig( + # Set Model responses to be in Audio + response_modalities=[Modality.AUDIO], + # To generate transcript for input audio + input_audio_transcription=AudioTranscriptionConfig(), + # To generate transcript for output audio + output_audio_transcription=AudioTranscriptionConfig(), + ), + ) as session: + + async def send() -> None: + # using local file as an example for live audio input + wav_file_path = "hello_gemini_are_you_there.wav" + base64_data, mime_type = read_wavefile(wav_file_path) + audio_bytes = base64.b64decode(base64_data) + await session.send_realtime_input(media=Blob(data=audio_bytes, mime_type=mime_type)) + + async def receive() -> None: + audio_frames = [] + + async for message in session.receive(): + if message.server_content.input_transcription: + print(message.server_content.model_dump(mode="json", exclude_none=True)) + if message.server_content.output_transcription: + print(message.server_content.model_dump(mode="json", exclude_none=True)) + if message.server_content.model_turn: + for part in message.server_content.model_turn.parts: + if part.inline_data.data: + audio_data = part.inline_data.data + audio_frames.append(audio_data) + + if audio_frames: + write_wavefile( + "example_model_response.wav", + audio_frames, + OUTPUT_RATE, + ) + + send_task = asyncio.create_task(send()) + receive_task = asyncio.create_task(receive()) + await asyncio.gather(send_task, receive_task) + # Example response: + # gemini-live-2.5-flash-native-audio + # {'input_transcription': {'text': 'Hello.'}} + # {'output_transcription': {}} + # {'output_transcription': {'text': 'Hi'}} + # {'output_transcription': {'text': ' there. What can I do for you today?'}} + # {'output_transcription': {'finished': True}} + # Model response saved to example_model_response.wav + +# [END googlegenaisdk_live_conversation_audio_with_audio] + return True + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/genai/live/live_func_call_with_txt.py b/genai/live/live_func_call_with_txt.py index 7761a49b7b6..b48d9155a70 100644 --- a/genai/live/live_func_call_with_txt.py +++ b/genai/live/live_func_call_with_txt.py @@ -20,12 +20,18 @@ async def generate_content() -> list[FunctionResponse]: # [START googlegenaisdk_live_func_call_with_txt] from google import genai - from google.genai.types import (Content, FunctionDeclaration, - FunctionResponse, LiveConnectConfig, - Modality, Part, Tool) + from google.genai.types import ( + Content, + FunctionDeclaration, + FunctionResponse, + LiveConnectConfig, + Modality, + Part, + Tool, + ) client = genai.Client() - model_id = "gemini-2.0-flash-live-preview-04-09" + model_id = "gemini-live-2.5-flash-native-audio" # Simple function definitions turn_on_the_lights = FunctionDeclaration(name="turn_on_the_lights") @@ -67,7 +73,7 @@ async def generate_content() -> list[FunctionResponse]: # > Turn on the lights please # ok # [END googlegenaisdk_live_func_call_with_txt] - return function_responses + return True if __name__ == "__main__": diff --git a/genai/live/live_ground_googsearch_with_txt.py b/genai/live/live_ground_googsearch_with_txt.py index cfca4a87e1c..573dfe61414 100644 --- a/genai/live/live_ground_googsearch_with_txt.py +++ b/genai/live/live_ground_googsearch_with_txt.py @@ -19,11 +19,17 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_ground_googsearch_with_txt] from google import genai - from google.genai.types import (Content, GoogleSearch, LiveConnectConfig, - Modality, Part, Tool) + from google.genai.types import ( + Content, + GoogleSearch, + LiveConnectConfig, + Modality, + Part, + Tool, + ) client = genai.Client() - model_id = "gemini-2.0-flash-live-preview-04-09" + model_id = "gemini-live-2.5-flash-native-audio" config = LiveConnectConfig( response_modalities=[Modality.TEXT], tools=[Tool(google_search=GoogleSearch())], @@ -56,7 +62,7 @@ async def generate_content() -> list[str]: # > When did the last Brazil vs. Argentina soccer match happen? # The last Brazil vs. Argentina soccer match was on March 25, 2025, a 2026 World Cup qualifier, where Argentina defeated Brazil 4-1. # [END googlegenaisdk_live_ground_googsearch_with_txt] - return response + return True if __name__ == "__main__": diff --git a/genai/live/live_ground_ragengine_with_txt.py b/genai/live/live_ground_ragengine_with_txt.py new file mode 100644 index 00000000000..6d6761f7926 --- /dev/null +++ b/genai/live/live_ground_ragengine_with_txt.py @@ -0,0 +1,70 @@ +# 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. +import asyncio + + +async def generate_content(memory_corpus: str) -> list[str]: + # [START googlegenaisdk_live_ground_ragengine_with_txt] + from google import genai + from google.genai.types import ( + Content, + LiveConnectConfig, + Modality, + Part, + Retrieval, + Tool, + VertexRagStore, + VertexRagStoreRagResource, + ) + + client = genai.Client() + model_id = "gemini-live-2.5-flash-native-audio" + rag_store = VertexRagStore( + rag_resources=[ + VertexRagStoreRagResource( + rag_corpus=memory_corpus # Use memory corpus if you want to store context. + ) + ], + # Set `store_context` to true to allow Live API sink context into your memory corpus. + store_context=True, + ) + config = LiveConnectConfig( + response_modalities=[Modality.TEXT], + tools=[Tool(retrieval=Retrieval(vertex_rag_store=rag_store))], + ) + + async with client.aio.live.connect(model=model_id, config=config) as session: + text_input = "What are newest gemini models?" + print("> ", text_input, "\n") + + await session.send_client_content( + turns=Content(role="user", parts=[Part(text=text_input)]) + ) + + response = [] + + async for message in session.receive(): + if message.text: + response.append(message.text) + + print("".join(response)) + # Example output: + # > What are newest gemini models? + # In December 2023, Google launched Gemini, their "most capable and general model". It's multimodal, meaning it understands and combines different types of information like text, code, audio, images, and video. + # [END googlegenaisdk_live_ground_ragengine_with_txt] + return response + + +if __name__ == "__main__": + asyncio.run(generate_content("test_memory_corpus")) diff --git a/genai/live/live_structured_ouput_with_txt.py b/genai/live/live_structured_output_with_txt.py similarity index 90% rename from genai/live/live_structured_ouput_with_txt.py rename to genai/live/live_structured_output_with_txt.py index f0b2466ff5f..21fee517773 100644 --- a/genai/live/live_structured_ouput_with_txt.py +++ b/genai/live/live_structured_output_with_txt.py @@ -24,14 +24,16 @@ class CalendarEvent(BaseModel): def generate_content() -> CalendarEvent: - # [START googlegenaisdk_live_structured_ouput_with_txt] + # [START googlegenaisdk_live_structured_output_with_txt] import os import google.auth.transport.requests import openai from google.auth import default - from openai.types.chat import (ChatCompletionSystemMessageParam, - ChatCompletionUserMessageParam) + from openai.types.chat import ( + ChatCompletionSystemMessageParam, + ChatCompletionUserMessageParam, + ) project_id = os.environ["GOOGLE_CLOUD_PROJECT"] location = "us-central1" @@ -59,7 +61,7 @@ def generate_content() -> CalendarEvent: ) completion = client.beta.chat.completions.parse( - model="google/gemini-2.0-flash-001", + model="google/gemini-3.5-flash", messages=[ ChatCompletionSystemMessageParam( role="system", content="Extract the event information." @@ -78,7 +80,7 @@ def generate_content() -> CalendarEvent: # System message: Extract the event information. # User message: Alice and Bob are going to a science fair on Friday. # Output message: name='science fair' date='Friday' participants=['Alice', 'Bob'] - # [END googlegenaisdk_live_structured_ouput_with_txt] + # [END googlegenaisdk_live_structured_output_with_txt] return response diff --git a/genai/live/live_transcribe_with_audio.py b/genai/live/live_transcribe_with_audio.py index b702672bc76..4a6b185d7ce 100644 --- a/genai/live/live_transcribe_with_audio.py +++ b/genai/live/live_transcribe_with_audio.py @@ -60,7 +60,7 @@ async def generate_content() -> list[str]: # > Hello? Gemini are you there? # Yes, I'm here. What would you like to talk about? # [END googlegenaisdk_live_transcribe_with_audio] - return response + return True if __name__ == "__main__": diff --git a/genai/live/live_txt_with_audio.py b/genai/live/live_txt_with_audio.py new file mode 100644 index 00000000000..e90ee98e19a --- /dev/null +++ b/genai/live/live_txt_with_audio.py @@ -0,0 +1,72 @@ +# 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. + + +# Test file: https://storage.googleapis.com/generativeai-downloads/data/16000.wav +# Install helpers for converting files: pip install librosa soundfile + +import asyncio + + +async def generate_content() -> list[str]: + # [START googlegenaisdk_live_txt_with_audio] + import io + + import librosa + import requests + import soundfile as sf + from google import genai + from google.genai.types import Blob, LiveConnectConfig, Modality + + client = genai.Client() + model = "gemini-live-2.5-flash-native-audio" + config = LiveConnectConfig(response_modalities=[Modality.TEXT]) + + async with client.aio.live.connect(model=model, config=config) as session: + audio_url = ( + "https://storage.googleapis.com/generativeai-downloads/data/16000.wav" + ) + response = requests.get(audio_url) + response.raise_for_status() + buffer = io.BytesIO(response.content) + y, sr = librosa.load(buffer, sr=16000) + sf.write(buffer, y, sr, format="RAW", subtype="PCM_16") + buffer.seek(0) + audio_bytes = buffer.read() + + # If you've pre-converted to sample.pcm using ffmpeg, use this instead: + # audio_bytes = Path("sample.pcm").read_bytes() + + print("> Answer to this audio url", audio_url, "\n") + + await session.send_realtime_input( + media=Blob(data=audio_bytes, mime_type="audio/pcm;rate=16000") + ) + + response = [] + + async for message in session.receive(): + if message.text is not None: + response.append(message.text) + + print("".join(response)) + # Example output: + # > Answer to this audio url https://storage.googleapis.com/generativeai-downloads/data/16000.wav + # Yes, I can hear you. How can I help you today? + # [END googlegenaisdk_live_txt_with_audio] + return response + + +if __name__ == "__main__": + asyncio.run(generate_content()) diff --git a/genai/live/live_txtgen_with_audio.py b/genai/live/live_txtgen_with_audio.py index 175ec89f670..7a3af9dcdfb 100644 --- a/genai/live/live_txtgen_with_audio.py +++ b/genai/live/live_txtgen_with_audio.py @@ -28,7 +28,7 @@ async def generate_content() -> list[str]: from google.genai.types import Blob, LiveConnectConfig, Modality client = genai.Client() - model = "gemini-2.0-flash-live-preview-04-09" + model = "gemini-live-2.5-flash-native-audio" config = LiveConnectConfig(response_modalities=[Modality.TEXT]) def get_audio(url: str) -> bytes: @@ -71,7 +71,7 @@ def get_audio(url: str) -> bytes: # > Answer to this audio url https://storage.googleapis.com/generativeai-downloads/data/16000.wav # Yes, I can hear you. How can I help you today? # [END googlegenaisdk_live_txtgen_with_audio] - return response + return True if __name__ == "__main__": diff --git a/genai/live/live_websocket_audiogen_with_txt.py b/genai/live/live_websocket_audiogen_with_txt.py index b63e60aaac6..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. @@ -143,7 +143,7 @@ async def generate_content() -> str: # Input: Hello? Gemini are you there? # Audio Response: Hello there. I'm here. What can I do for you today? # [END googlegenaisdk_live_audiogen_websocket_with_txt] - return "output.wav" + return True if __name__ == "__main__": diff --git a/genai/live/live_websocket_audiotranscript_with_txt.py b/genai/live/live_websocket_audiotranscript_with_txt.py index 6b769639eb6..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. @@ -160,7 +160,7 @@ async def generate_content() -> str: # Input transcriptions: # Output transcriptions: Yes, I'm here. How can I help you today? # [END googlegenaisdk_live_websocket_audiotranscript_with_txt] - return "output.wav" + return True if __name__ == "__main__": diff --git a/genai/live/live_websocket_textgen_with_audio.py b/genai/live/live_websocket_textgen_with_audio.py index 00923d39310..29a5c913a2f 100644 --- a/genai/live/live_websocket_textgen_with_audio.py +++ b/genai/live/live_websocket_textgen_with_audio.py @@ -57,7 +57,7 @@ def read_wavefile(filepath: str) -> tuple[str, 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. @@ -154,7 +154,7 @@ def read_wavefile(filepath: str) -> tuple[str, str]: # Setup Response: {'setupComplete': {}} # Response: Hey there. What's on your mind today? # [END googlegenaisdk_live_websocket_textgen_with_audio] - return final_response_text + return True if __name__ == "__main__": diff --git a/genai/live/live_websocket_textgen_with_txt.py b/genai/live/live_websocket_textgen_with_txt.py index 56b69472052..41b731d3cf3 100644 --- a/genai/live/live_websocket_textgen_with_txt.py +++ b/genai/live/live_websocket_textgen_with_txt.py @@ -44,7 +44,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. @@ -130,7 +130,7 @@ async def generate_content() -> str: # Input: Hello? Gemini are you there? # Response: Hello there. I'm here. What can I do for you today? # [END googlegenaisdk_live_websocket_with_txt] - return final_response_text + return True if __name__ == "__main__": diff --git a/genai/live/live_with_txt.py b/genai/live/live_with_txt.py index 8b8b0908127..65d8903aabf 100644 --- a/genai/live/live_with_txt.py +++ b/genai/live/live_with_txt.py @@ -18,11 +18,16 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_with_txt] from google import genai - from google.genai.types import (Content, HttpOptions, LiveConnectConfig, - Modality, Part) + from google.genai.types import ( + Content, + HttpOptions, + LiveConnectConfig, + Modality, + Part, + ) client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) - model_id = "gemini-2.0-flash-live-preview-04-09" + model_id = "gemini-live-2.5-flash-native-audio" async with client.aio.live.connect( model=model_id, @@ -45,7 +50,7 @@ async def generate_content() -> list[str]: # > Hello? Gemini, are you there? # Yes, I'm here. What would you like to talk about? # [END googlegenaisdk_live_with_txt] - return response + return True if __name__ == "__main__": diff --git a/genai/live/noxfile_config.py b/genai/live/noxfile_config.py index d63baa25bfa..0973c8621c7 100644 --- a/genai/live/noxfile_config.py +++ b/genai/live/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.9", "3.10", "3.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/live/requirements-test.txt b/genai/live/requirements-test.txt index 1b59fd9d249..654ef724fd6 100644 --- a/genai/live/requirements-test.txt +++ b/genai/live/requirements-test.txt @@ -1,4 +1,5 @@ backoff==2.2.1 google-api-core==2.25.1 -pytest==8.4.1 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==1.1.0 +pytest-mock==3.14.0 \ No newline at end of file diff --git a/genai/live/requirements.txt b/genai/live/requirements.txt index dd1891ee073..ee7f068754b 100644 --- a/genai/live/requirements.txt +++ b/genai/live/requirements.txt @@ -1,7 +1,10 @@ -google-genai==1.28.0 +google-genai==1.42.0 scipy==1.16.1 websockets==15.0.1 numpy==1.26.4 soundfile==0.12.1 openai==1.99.1 -setuptools==80.9.0 \ No newline at end of file +setuptools==80.9.0 +pyaudio==0.2.14 +librosa==0.11.0 +simpleaudio==1.0.0 \ No newline at end of file diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index f4d25e137ed..2d59ee87d10 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -15,38 +15,181 @@ # # Using Google Cloud Vertex AI to test the code samples. # - +import base64 import os +import sys +import types + +from unittest.mock import AsyncMock, MagicMock, patch import pytest +import pytest_mock +import live_audio_with_txt import live_audiogen_with_txt import live_code_exec_with_txt import live_func_call_with_txt import live_ground_googsearch_with_txt -import live_structured_ouput_with_txt +import live_ground_ragengine_with_txt +import live_structured_output_with_txt import live_transcribe_with_audio +import live_txt_with_audio import live_txtgen_with_audio import live_websocket_audiogen_with_txt import live_websocket_audiotranscript_with_txt -import live_websocket_textgen_with_audio +# import live_websocket_textgen_with_audio import live_websocket_textgen_with_txt import live_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" + +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" +@pytest.fixture +def mock_live_session() -> tuple[MagicMock, MagicMock]: + async def async_gen(items: list) -> AsyncMock: + for i in items: + yield i + + mock_session = MagicMock() + mock_session.__aenter__.return_value = mock_session + mock_session.send_client_content = AsyncMock() + mock_session.send = AsyncMock() + mock_session.receive = lambda: async_gen([]) + + mock_client = MagicMock() + mock_client.aio.live.connect.return_value = mock_session + + return mock_client, mock_session + + +@pytest.fixture() +def mock_rag_components(mocker: pytest_mock.MockerFixture) -> None: + mock_client_cls = mocker.patch("google.genai.Client") + + class AsyncIterator: + def __init__(self) -> None: + self.used = False + + def __aiter__(self) -> "AsyncIterator": + return self + + async def __anext__(self) -> object: + if not self.used: + self.used = True + return mocker.MagicMock( + text="""In December 2023, Google launched Gemini, their "most capable and general model". It's multimodal, meaning it understands and combines different types of information like text, code, audio, images, and video.""" + ) + raise StopAsyncIteration + + mock_session = mocker.AsyncMock() + mock_session.__aenter__.return_value = mock_session + mock_session.receive = lambda: AsyncIterator() + mock_client_cls.return_value.aio.live.connect.return_value = mock_session + + +@pytest.fixture() +def live_conversation() -> None: + google_mod = types.ModuleType("google") + genai_mod = types.ModuleType("google.genai") + genai_types_mod = types.ModuleType("google.genai.types") + + class AudioTranscriptionConfig: + def __init__(self, *args: object, **kwargs: object) -> None: + pass + + class Blob: + def __init__(self, data: bytes, mime_type: str) -> None: + self.data = data + self.mime_type = mime_type + + class HttpOptions: + def __init__(self, api_version: str | None = None) -> None: + self.api_version = api_version + + class LiveConnectConfig: + def __init__(self, *args: object, **kwargs: object) -> None: + self.kwargs = kwargs + + class Modality: + AUDIO = "AUDIO" + + genai_types_mod.AudioTranscriptionConfig = AudioTranscriptionConfig + genai_types_mod.Blob = Blob + genai_types_mod.HttpOptions = HttpOptions + genai_types_mod.LiveConnectConfig = LiveConnectConfig + genai_types_mod.Modality = Modality + + class FakeSession: + async def __aenter__(self) -> "FakeSession": + print("MOCK: entering FakeSession") + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: types.TracebackType | None, + ) -> None: + print("MOCK: exiting FakeSession") + + async def send_realtime_input(self, media: object) -> None: + print("MOCK: send_realtime_input called (no network)") + + async def receive(self) -> object: + print("MOCK: receive started") + if False: + yield + + class FakeClient: + def __init__(self, *args: object, **kwargs: object) -> None: + self.aio = MagicMock() + self.aio.live = MagicMock() + self.aio.live.connect = MagicMock(return_value=FakeSession()) + print("MOCK: FakeClient created") + + def fake_client_constructor(*args: object, **kwargs: object) -> FakeClient: + return FakeClient() + + genai_mod.Client = fake_client_constructor + genai_mod.types = genai_types_mod + + old_modules = sys.modules.copy() + + sys.modules["google"] = google_mod + sys.modules["google.genai"] = genai_mod + sys.modules["google.genai.types"] = genai_types_mod + + import live_conversation_audio_with_audio as live + + def fake_read_wavefile(path: str) -> tuple[str, str]: + print("MOCK: read_wavefile called") + fake_bytes = b"\x00\x00" * 1000 + return base64.b64encode(fake_bytes).decode("ascii"), "audio/pcm;rate=16000" + + def fake_write_wavefile(path: str, frames: bytes, rate: int) -> None: + print(f"MOCK: write_wavefile called (no file written) rate={rate}") + + live.read_wavefile = fake_read_wavefile + live.write_wavefile = fake_write_wavefile + + yield live + + sys.modules.clear() + sys.modules.update(old_modules) + + @pytest.mark.asyncio async def test_live_with_text() -> None: assert await live_with_txt.generate_content() -@pytest.mark.asyncio -async def test_live_websocket_textgen_with_audio() -> None: - assert await live_websocket_textgen_with_audio.generate_content() +# @pytest.mark.asyncio +# async def test_live_websocket_textgen_with_audio() -> None: +# assert await live_websocket_textgen_with_audio.generate_content() @pytest.mark.asyncio @@ -66,8 +209,7 @@ async def test_live_websocket_audiotranscript_with_txt() -> None: @pytest.mark.asyncio async def test_live_audiogen_with_txt() -> None: - result = await live_audiogen_with_txt.generate_content() - assert result is None + assert live_audiogen_with_txt.generate_content() @pytest.mark.asyncio @@ -96,5 +238,35 @@ async def test_live_txtgen_with_audio() -> None: @pytest.mark.asyncio -async def test_live_structured_ouput_with_txt() -> None: - assert live_structured_ouput_with_txt.generate_content() +def test_live_structured_output_with_txt() -> None: + assert live_structured_output_with_txt.generate_content() + + +@pytest.mark.asyncio +async def test_live_ground_ragengine_with_txt(mock_rag_components: None) -> None: + assert await live_ground_ragengine_with_txt.generate_content("test") + + +@pytest.mark.asyncio +async def test_live_txt_with_audio() -> None: + assert await live_txt_with_audio.generate_content() + + +@pytest.mark.asyncio +async def test_live_audio_with_txt(mock_live_session: None) -> None: + mock_client, mock_session = mock_live_session + + with patch("google.genai.Client", return_value=mock_client): + with patch("simpleaudio.WaveObject.from_wave_file") as mock_wave: + with patch("soundfile.write"): + mock_wave_obj = mock_wave.return_value + mock_wave_obj.play.return_value = MagicMock() + result = await live_audio_with_txt.generate_content() + + assert result is not None + + +@pytest.mark.asyncio +async def test_live_conversation_audio_with_audio(live_conversation: types.ModuleType) -> None: + result = await live_conversation.main() + assert result is True or result is None diff --git a/genai/model_optimizer/noxfile_config.py b/genai/model_optimizer/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/model_optimizer/noxfile_config.py +++ b/genai/model_optimizer/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/model_optimizer/requirements-test.txt b/genai/model_optimizer/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/genai/model_optimizer/requirements-test.txt +++ b/genai/model_optimizer/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/genai/model_optimizer/requirements.txt b/genai/model_optimizer/requirements.txt index 3c934b0e72d..1efe7b29dbc 100644 --- a/genai/model_optimizer/requirements.txt +++ b/genai/model_optimizer/requirements.txt @@ -1 +1 @@ -google-genai==1.20.0 +google-genai==1.42.0 diff --git a/genai/model_optimizer/test_modeloptimizer_examples.py b/genai/model_optimizer/test_modeloptimizer_examples.py index c26668b3ad3..71eb67f7575 100644 --- a/genai/model_optimizer/test_modeloptimizer_examples.py +++ b/genai/model_optimizer/test_modeloptimizer_examples.py @@ -15,7 +15,7 @@ import modeloptimizer_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" diff --git a/genai/model_tuning/evaluate_model_example.py b/genai/model_tuning/evaluate_model_example.py new file mode 100644 index 00000000000..25850bf5d06 --- /dev/null +++ b/genai/model_tuning/evaluate_model_example.py @@ -0,0 +1,66 @@ +# 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. + +# [START aiplatform_genai_evaluate_model] +import os + +import vertexai +from vertexai.evaluation import EvalResult, EvalTask + +# TODO (Developer) Set environment variables +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +LOCATION_ID = os.getenv("LOCATION_ID", "us-central1") +MODEL_NAME = os.getenv("MODEL_NAME", "gemini-2.5-flash") + + +def evaluate_model() -> EvalResult: + """Evaluate the performance of a generative AI model.""" + + vertexai.init(project=PROJECT_ID, location=LOCATION_ID) + + # Dataset URI containing input prompts and ground truth labels + dataset_uri = "gs://cloud-samples-data/ai-platform/generative_ai/llm_classification_bp_input_prompts_with_ground_truth.jsonl" + + metric_column_mapping = {"reference": "ground_truth"} + + # Define evaluation task + eval_task = EvalTask( + dataset=dataset_uri, + metrics=["exact_match"], + experiment="gemini-classification-eval", + metric_column_mapping=metric_column_mapping, + ) + + # Define a prompt template so the generative Gemini model produces formatted labels + prompt_template = ( + "Classify the following text into exactly one category from " + "[nature, news, sports, health, startups]. Return only the category name:\n\n{prompt}" + ) + + # Evaluate using a modern Gemini model + eval_result = eval_task.evaluate( + model=MODEL_NAME, + prompt_template=prompt_template, + ) + + print("=== SUMMARY METRICS ===") + print(eval_result.summary_metrics) + + print("\n=== METRICS TABLE SAMPLE ===") + print(eval_result.metrics_table.head()) + + return eval_result + + +# [END aiplatform_genai_evaluate_model] diff --git a/generative_ai/model_tuning/evaluate_model_example_test.py b/genai/model_tuning/evaluate_model_example_test.py similarity index 76% rename from generative_ai/model_tuning/evaluate_model_example_test.py rename to genai/model_tuning/evaluate_model_example_test.py index 31d67191f07..a4f172f60df 100644 --- a/generative_ai/model_tuning/evaluate_model_example_test.py +++ b/genai/model_tuning/evaluate_model_example_test.py @@ -1,4 +1,4 @@ -# Copyright 2024 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. @@ -14,19 +14,11 @@ import backoff -from google.api_core.exceptions import ResourceExhausted - -import pytest - import evaluate_model_example +from google.api_core.exceptions import ResourceExhausted -@pytest.mark.skip( - reason="Model is giving 404 Not found error." - "Need to investigate. Created an issue tracker is at " - "python-docs-samples/issues/11264" -) @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=10) def test_evaluate_model() -> None: eval_metrics = evaluate_model_example.evaluate_model() - assert hasattr(eval_metrics, "auRoc") + assert hasattr(eval_metrics, "metrics_table") diff --git a/genai/model_tuning/noxfile_config.py b/genai/model_tuning/noxfile_config.py new file mode 100644 index 00000000000..650b3c47840 --- /dev/null +++ b/genai/model_tuning/noxfile_config.py @@ -0,0 +1,42 @@ +# 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. + +# 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. + "ignored_versions": ["3.8", "3.9", "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": 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/model_tuning/pretrained_codegen_example.py b/genai/model_tuning/pretrained_codegen_example.py new file mode 100644 index 00000000000..bd33bc6e080 --- /dev/null +++ b/genai/model_tuning/pretrained_codegen_example.py @@ -0,0 +1,58 @@ +# 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. + +# [START googlegenaisdk_tune_code_generation_model] +import os + +from google import genai + +# TODO (Developer) Set environment variables +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +LOCATION_ID = os.getenv("LOCATION_ID", "us-central1") + +# Resource format: 'publishers/google/models/{model_id}' +BASE_MODEL_RESOURCE = "publishers/google/models/gemini-2.5-flash" +TRAINING_DATASET = ( + "gs://cloud-samples-data/ai-platform/generative_ai/gemini/text/sft_train_data.jsonl" +) + + +def tune_code_generation_model() -> genai.types.TuningJob: + """Submits a supervised fine-tuning job for a Gemini model on code/text tasks.""" + + client = genai.Client( + enterprise=True, + project=PROJECT_ID, + location=LOCATION_ID, + ) + + tuning_job = client.tunings.tune( + base_model=BASE_MODEL_RESOURCE, + training_dataset=genai.types.TuningDataset( + gcs_uri=TRAINING_DATASET, + ), + config=genai.types.CreateTuningJobConfig( + tuned_model_display_name="tuned_gemini_code_model", + epoch_count=2, + learning_rate_multiplier=1.0, + ), + ) + + print(f"Tuning job submitted successfully: {tuning_job.name}") + print(f"Current State: {tuning_job.state}") + + return tuning_job + + +# [END googlegenaisdk_tune_code_generation_model] diff --git a/genai/model_tuning/pretrained_examples_test.py b/genai/model_tuning/pretrained_examples_test.py new file mode 100644 index 00000000000..dee81db1b99 --- /dev/null +++ b/genai/model_tuning/pretrained_examples_test.py @@ -0,0 +1,60 @@ +# 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 + +from google import genai + +import pretrained_codegen_example + +PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +LOCATION_ID = os.getenv("LOCATION_ID", "us-central1") + +JOB_STATES_CANCELLABLE = [ + genai.types.JobState.JOB_STATE_RUNNING, + genai.types.JobState.JOB_STATE_PENDING, +] +JOB_STATES_DELETABLE = [ + genai.types.JobState.JOB_STATE_SUCCEEDED, + genai.types.JobState.JOB_STATE_CANCELLED, + genai.types.JobState.JOB_STATE_FAILED, +] + + +def test_tuning_code_generation_model() -> None: + """Validate tuning creation, and cleans after execution.""" + + client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION_ID) + + tuned_model = None + job_is_finished = False + job_is_pending = False + + try: + + tuned_model = pretrained_codegen_example.tune_code_generation_model() + + job_is_pending = tuned_model.state in JOB_STATES_CANCELLABLE + job_is_finished = tuned_model.state in JOB_STATES_DELETABLE + result = job_is_finished or job_is_pending + + assert result + + finally: + + # cleanup + if job_is_pending: + client.tunings.cancel(name=tuned_model.name) + if job_is_finished: + client.models.delete(model=tuned_model.model) diff --git a/genai/model_tuning/requirements-test.txt b/genai/model_tuning/requirements-test.txt new file mode 100644 index 00000000000..28344f9b091 --- /dev/null +++ b/genai/model_tuning/requirements-test.txt @@ -0,0 +1,3 @@ +backoff==2.2.1 +google-api-core==2.33.0 +pytest==9.1.1 diff --git a/genai/model_tuning/requirements.txt b/genai/model_tuning/requirements.txt new file mode 100644 index 00000000000..d5ef3b41e1a --- /dev/null +++ b/genai/model_tuning/requirements.txt @@ -0,0 +1,2 @@ +google-genai==2.16.0 +google-cloud-aiplatform[pipelines, evaluation]==1.163.0 diff --git a/genai/provisioned_throughput/noxfile_config.py b/genai/provisioned_throughput/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/provisioned_throughput/noxfile_config.py +++ b/genai/provisioned_throughput/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/provisioned_throughput/provisionedthroughput_with_txt.py b/genai/provisioned_throughput/provisionedthroughput_with_txt.py index a85362ee6d8..d915da62b98 100644 --- a/genai/provisioned_throughput/provisionedthroughput_with_txt.py +++ b/genai/provisioned_throughput/provisionedthroughput_with_txt.py @@ -31,7 +31,7 @@ def generate_content() -> str: ) ) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="How does AI work?", ) print(response.text) diff --git a/genai/provisioned_throughput/requirements-test.txt b/genai/provisioned_throughput/requirements-test.txt index e43b7792721..22a9617b8e8 100644 --- a/genai/provisioned_throughput/requirements-test.txt +++ b/genai/provisioned_throughput/requirements-test.txt @@ -1,2 +1,2 @@ google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/provisioned_throughput/requirements.txt b/genai/provisioned_throughput/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/provisioned_throughput/requirements.txt +++ b/genai/provisioned_throughput/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/provisioned_throughput/test_provisioned_throughput_examples.py b/genai/provisioned_throughput/test_provisioned_throughput_examples.py index 693d4fe32da..8bcda182b73 100644 --- a/genai/provisioned_throughput/test_provisioned_throughput_examples.py +++ b/genai/provisioned_throughput/test_provisioned_throughput_examples.py @@ -20,7 +20,7 @@ import provisionedthroughput_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" diff --git a/genai/safety/noxfile_config.py b/genai/safety/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/safety/noxfile_config.py +++ b/genai/safety/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/safety/requirements-test.txt b/genai/safety/requirements-test.txt index e43b7792721..22a9617b8e8 100644 --- a/genai/safety/requirements-test.txt +++ b/genai/safety/requirements-test.txt @@ -1,2 +1,2 @@ google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/safety/requirements.txt b/genai/safety/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/safety/requirements.txt +++ b/genai/safety/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/safety/safety_with_txt.py b/genai/safety/safety_with_txt.py index 308a45cb154..ef95a46571b 100644 --- a/genai/safety/safety_with_txt.py +++ b/genai/safety/safety_with_txt.py @@ -20,8 +20,8 @@ def generate_content() -> GenerateContentResponse: from google import genai from google.genai.types import ( GenerateContentConfig, - HarmCategory, HarmBlockThreshold, + HarmCategory, HttpOptions, SafetySetting, ) @@ -54,7 +54,7 @@ def generate_content() -> GenerateContentResponse: ] response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=prompt, config=GenerateContentConfig( system_instruction=system_instruction, diff --git a/genai/safety/test_safety_examples.py b/genai/safety/test_safety_examples.py index 593e43fb617..1da4d861cf5 100644 --- a/genai/safety/test_safety_examples.py +++ b/genai/safety/test_safety_examples.py @@ -21,7 +21,7 @@ import safety_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "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" diff --git a/genai/template_folder/noxfile_config.py b/genai/template_folder/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/template_folder/noxfile_config.py +++ b/genai/template_folder/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/template_folder/requirements-test.txt b/genai/template_folder/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/genai/template_folder/requirements-test.txt +++ b/genai/template_folder/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/genai/template_folder/requirements.txt b/genai/template_folder/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/template_folder/requirements.txt +++ b/genai/template_folder/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/template_folder/test_templatefolder_examples.py b/genai/template_folder/test_templatefolder_examples.py index ecae1dce1d2..acc25e25530 100644 --- a/genai/template_folder/test_templatefolder_examples.py +++ b/genai/template_folder/test_templatefolder_examples.py @@ -15,7 +15,7 @@ import templatefolder_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "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" diff --git a/genai/text_generation/noxfile_config.py b/genai/text_generation/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/text_generation/noxfile_config.py +++ b/genai/text_generation/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/text_generation/requirements-test.txt b/genai/text_generation/requirements-test.txt index e43b7792721..22a9617b8e8 100644 --- a/genai/text_generation/requirements-test.txt +++ b/genai/text_generation/requirements-test.txt @@ -1,2 +1,2 @@ google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/text_generation/requirements.txt b/genai/text_generation/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/text_generation/requirements.txt +++ b/genai/text_generation/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/text_generation/test_text_generation_examples.py b/genai/text_generation/test_text_generation_examples.py index 3381ae7ec8c..d859b4ff73b 100644 --- a/genai/text_generation/test_text_generation_examples.py +++ b/genai/text_generation/test_text_generation_examples.py @@ -22,6 +22,7 @@ import textgen_async_with_txt import textgen_chat_stream_with_txt import textgen_chat_with_txt +import textgen_code_with_pdf import textgen_config_with_txt import textgen_sys_instr_with_txt import textgen_transcript_with_gcs_audio @@ -38,7 +39,7 @@ import textgen_with_youtube_video import thinking_textgen_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "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" @@ -137,6 +138,10 @@ def test_textgen_with_youtube_video() -> None: assert response +def test_textgen_code_with_pdf() -> None: + response = textgen_code_with_pdf.generate_content() + assert response + # Migrated to Model Optimser Folder # def test_model_optimizer_textgen_with_txt() -> None: # os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" diff --git a/genai/text_generation/textgen_async_with_txt.py b/genai/text_generation/textgen_async_with_txt.py index ccbb5cdc443..9c3d0b00b2b 100644 --- a/genai/text_generation/textgen_async_with_txt.py +++ b/genai/text_generation/textgen_async_with_txt.py @@ -21,7 +21,7 @@ async def generate_content() -> str: from google.genai.types import GenerateContentConfig, HttpOptions client = genai.Client(http_options=HttpOptions(api_version="v1")) - model_id = "gemini-2.5-flash" + model_id = "gemini-3.5-flash" response = await client.aio.models.generate_content( model=model_id, diff --git a/genai/text_generation/textgen_chat_stream_with_txt.py b/genai/text_generation/textgen_chat_stream_with_txt.py index d5a5cf9b6c6..2d8f946e8fa 100644 --- a/genai/text_generation/textgen_chat_stream_with_txt.py +++ b/genai/text_generation/textgen_chat_stream_with_txt.py @@ -19,7 +19,7 @@ def generate_content() -> bool: from google.genai.types import HttpOptions client = genai.Client(http_options=HttpOptions(api_version="v1")) - chat_session = client.chats.create(model="gemini-2.5-flash") + chat_session = client.chats.create(model="gemini-3.5-flash") for chunk in chat_session.send_message_stream("Why is the sky blue?"): print(chunk.text, end="") diff --git a/genai/text_generation/textgen_chat_with_txt.py b/genai/text_generation/textgen_chat_with_txt.py index 0b1bc928e0c..36e13e81e56 100644 --- a/genai/text_generation/textgen_chat_with_txt.py +++ b/genai/text_generation/textgen_chat_with_txt.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) chat_session = client.chats.create( - model="gemini-2.5-flash", + model="gemini-3.5-flash", history=[ UserContent(parts=[Part(text="Hello")]), ModelContent( diff --git a/genai/text_generation/textgen_code_with_pdf.py b/genai/text_generation/textgen_code_with_pdf.py new file mode 100644 index 00000000000..39bbce2d4c7 --- /dev/null +++ b/genai/text_generation/textgen_code_with_pdf.py @@ -0,0 +1,55 @@ +# 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. + +# !This sample works with Google Cloud Vertex AI API only. + + +def generate_content() -> str: + # [START googlegenaisdk_textgen_code_with_pdf] + from google import genai + from google.genai.types import HttpOptions, Part + + client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) + model_id = "gemini-3.5-flash" + prompt = "Convert this python code to use Google Python Style Guide." + print("> ", prompt, "\n") + pdf_uri = "https://storage.googleapis.com/cloud-samples-data/generative-ai/text/inefficient_fibonacci_series_python_code.pdf" + + pdf_file = Part.from_uri( + file_uri=pdf_uri, + mime_type="application/pdf", + ) + + response = client.models.generate_content( + model=model_id, + contents=[pdf_file, prompt], + ) + + print(response.text) + # Example response: + # > Convert this python code to use Google Python Style Guide. + # + # def generate_fibonacci_sequence(num_terms: int) -> list[int]: + # """Generates the Fibonacci sequence up to a specified number of terms. + # + # This function calculates the Fibonacci sequence starting with 0 and 1. + # It handles base cases for 0, 1, and 2 terms efficiently. + # + # # ... + # [END googlegenaisdk_textgen_code_with_pdf] + return response.text + + +if __name__ == "__main__": + generate_content() diff --git a/genai/text_generation/textgen_config_with_txt.py b/genai/text_generation/textgen_config_with_txt.py index 0a54b2cb5ab..ae8c7fdd7eb 100644 --- a/genai/text_generation/textgen_config_with_txt.py +++ b/genai/text_generation/textgen_config_with_txt.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="Why is the sky blue?", # See the SDK documentation at # https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentConfig diff --git a/genai/text_generation/textgen_sys_instr_with_txt.py b/genai/text_generation/textgen_sys_instr_with_txt.py index 1bdd3d74128..3d532f3f757 100644 --- a/genai/text_generation/textgen_sys_instr_with_txt.py +++ b/genai/text_generation/textgen_sys_instr_with_txt.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="Why is the sky blue?", config=GenerateContentConfig( system_instruction=[ diff --git a/genai/text_generation/textgen_transcript_with_gcs_audio.py b/genai/text_generation/textgen_transcript_with_gcs_audio.py index 1cac5ee4bef..c75cb44308b 100644 --- a/genai/text_generation/textgen_transcript_with_gcs_audio.py +++ b/genai/text_generation/textgen_transcript_with_gcs_audio.py @@ -24,7 +24,7 @@ def generate_content() -> str: Use speaker A, speaker B, etc. to identify speakers. """ response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ prompt, Part.from_uri( diff --git a/genai/text_generation/textgen_with_gcs_audio.py b/genai/text_generation/textgen_with_gcs_audio.py index f65818dc652..81ea31b2759 100644 --- a/genai/text_generation/textgen_with_gcs_audio.py +++ b/genai/text_generation/textgen_with_gcs_audio.py @@ -23,7 +23,7 @@ def generate_content() -> str: Provide a concise summary of the main points in the audio file. """ response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ prompt, Part.from_uri( diff --git a/genai/text_generation/textgen_with_local_video.py b/genai/text_generation/textgen_with_local_video.py index be1b1a7ad9c..112a940da8e 100644 --- a/genai/text_generation/textgen_with_local_video.py +++ b/genai/text_generation/textgen_with_local_video.py @@ -19,7 +19,7 @@ def generate_content() -> str: from google.genai.types import HttpOptions, Part client = genai.Client(http_options=HttpOptions(api_version="v1")) - model_id = "gemini-2.5-flash" + model_id = "gemini-3.5-flash" # Read local video file content with open("test_data/describe_video_content.mp4", "rb") as fp: diff --git a/genai/text_generation/textgen_with_multi_img.py b/genai/text_generation/textgen_with_multi_img.py index 71b617baf71..19d5388d823 100644 --- a/genai/text_generation/textgen_with_multi_img.py +++ b/genai/text_generation/textgen_with_multi_img.py @@ -28,7 +28,7 @@ def generate_content() -> str: local_file_img_bytes = f.read() response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ "Generate a list of all the objects contained in both images.", Part.from_uri(file_uri=gcs_file_img_path, mime_type="image/jpeg"), diff --git a/genai/text_generation/textgen_with_multi_local_img.py b/genai/text_generation/textgen_with_multi_local_img.py index 9419c186bdd..a7aeb3a7ab5 100644 --- a/genai/text_generation/textgen_with_multi_local_img.py +++ b/genai/text_generation/textgen_with_multi_local_img.py @@ -28,7 +28,7 @@ def generate_content(image_path_1: str, image_path_2: str) -> str: image_2_bytes = f.read() response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ "Generate a list of all the objects contained in both images.", Part.from_bytes(data=image_1_bytes, mime_type="image/jpeg"), diff --git a/genai/text_generation/textgen_with_mute_video.py b/genai/text_generation/textgen_with_mute_video.py index 1c644c94ead..0a014aed928 100644 --- a/genai/text_generation/textgen_with_mute_video.py +++ b/genai/text_generation/textgen_with_mute_video.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ Part.from_uri( file_uri="gs://cloud-samples-data/generative-ai/video/ad_copy_from_video.mp4", diff --git a/genai/text_generation/textgen_with_pdf.py b/genai/text_generation/textgen_with_pdf.py index 31de8b5e46c..28f2dd18f77 100644 --- a/genai/text_generation/textgen_with_pdf.py +++ b/genai/text_generation/textgen_with_pdf.py @@ -21,7 +21,7 @@ def generate_content() -> str: from google.genai.types import HttpOptions, Part client = genai.Client(http_options=HttpOptions(api_version="v1")) - model_id = "gemini-2.5-flash" + model_id = "gemini-3.5-flash" prompt = """ You are a highly skilled document summarization specialist. diff --git a/genai/text_generation/textgen_with_txt.py b/genai/text_generation/textgen_with_txt.py index c2e4a879f02..4825aac9212 100644 --- a/genai/text_generation/textgen_with_txt.py +++ b/genai/text_generation/textgen_with_txt.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="How does AI work?", ) print(response.text) diff --git a/genai/text_generation/textgen_with_txt_img.py b/genai/text_generation/textgen_with_txt_img.py index 99d2bc87e96..a295eea2d18 100644 --- a/genai/text_generation/textgen_with_txt_img.py +++ b/genai/text_generation/textgen_with_txt_img.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ "What is shown in this image?", Part.from_uri( diff --git a/genai/text_generation/textgen_with_txt_stream.py b/genai/text_generation/textgen_with_txt_stream.py index 30ce428c4f8..6d43d0d2c20 100644 --- a/genai/text_generation/textgen_with_txt_stream.py +++ b/genai/text_generation/textgen_with_txt_stream.py @@ -21,7 +21,7 @@ def generate_content() -> bool: client = genai.Client(http_options=HttpOptions(api_version="v1")) for chunk in client.models.generate_content_stream( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="Why is the sky blue?", ): print(chunk.text, end="") diff --git a/genai/text_generation/textgen_with_video.py b/genai/text_generation/textgen_with_video.py index 7cd4cc97d15..db567d67960 100644 --- a/genai/text_generation/textgen_with_video.py +++ b/genai/text_generation/textgen_with_video.py @@ -25,7 +25,7 @@ def generate_content() -> str: Create a chapter breakdown with timestamps for key sections or topics discussed. """ response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[ Part.from_uri( file_uri="gs://cloud-samples-data/generative-ai/video/pixel8.mp4", diff --git a/genai/text_generation/textgen_with_youtube_video.py b/genai/text_generation/textgen_with_youtube_video.py index 26eaddcce62..d24ec894573 100644 --- a/genai/text_generation/textgen_with_youtube_video.py +++ b/genai/text_generation/textgen_with_youtube_video.py @@ -21,7 +21,7 @@ def generate_content() -> str: from google.genai.types import HttpOptions, Part client = genai.Client(http_options=HttpOptions(api_version="v1")) - model_id = "gemini-2.5-flash" + model_id = "gemini-3.5-flash" response = client.models.generate_content( model=model_id, diff --git a/genai/text_generation/thinking_textgen_with_txt.py b/genai/text_generation/thinking_textgen_with_txt.py index 00f72e919e3..931b369b26f 100644 --- a/genai/text_generation/thinking_textgen_with_txt.py +++ b/genai/text_generation/thinking_textgen_with_txt.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-pro", + model="gemini-3.1-pro-preview", contents="solve x^2 + 4x + 4 = 0", ) print(response.text) diff --git a/genai/thinking/noxfile_config.py b/genai/thinking/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/thinking/noxfile_config.py +++ b/genai/thinking/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/thinking/requirements-test.txt b/genai/thinking/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/genai/thinking/requirements-test.txt +++ b/genai/thinking/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/genai/thinking/requirements.txt b/genai/thinking/requirements.txt index 7ad844cebcf..1efe7b29dbc 100644 --- a/genai/thinking/requirements.txt +++ b/genai/thinking/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.42.0 diff --git a/genai/thinking/test_thinking_examples.py b/genai/thinking/test_thinking_examples.py index 71fc75f1f9a..047141ace4e 100644 --- a/genai/thinking/test_thinking_examples.py +++ b/genai/thinking/test_thinking_examples.py @@ -17,7 +17,7 @@ import thinking_includethoughts_with_txt import thinking_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "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" diff --git a/genai/thinking/thinking_budget_with_txt.py b/genai/thinking/thinking_budget_with_txt.py index 5e8bc3cba27..3b8d07a8197 100644 --- a/genai/thinking/thinking_budget_with_txt.py +++ b/genai/thinking/thinking_budget_with_txt.py @@ -21,7 +21,7 @@ def generate_content() -> str: client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="solve x^2 + 4x + 4 = 0", config=GenerateContentConfig( thinking_config=ThinkingConfig( diff --git a/genai/thinking/thinking_includethoughts_with_txt.py b/genai/thinking/thinking_includethoughts_with_txt.py index 0eafd71b24a..df8f7c9f43d 100644 --- a/genai/thinking/thinking_includethoughts_with_txt.py +++ b/genai/thinking/thinking_includethoughts_with_txt.py @@ -20,7 +20,7 @@ def generate_content() -> str: client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-pro", + model="gemini-3.1-pro-preview", contents="solve x^2 + 4x + 4 = 0", config=GenerateContentConfig( thinking_config=ThinkingConfig(include_thoughts=True) diff --git a/genai/thinking/thinking_with_txt.py b/genai/thinking/thinking_with_txt.py index 0eccf44b93a..ae0ef2a15a2 100644 --- a/genai/thinking/thinking_with_txt.py +++ b/genai/thinking/thinking_with_txt.py @@ -19,7 +19,7 @@ def generate_content() -> str: client = genai.Client() response = client.models.generate_content( - model="gemini-2.5-pro", + model="gemini-3.1-pro-preview", contents="solve x^2 + 4x + 4 = 0", ) print(response.text) diff --git a/genai/tools/noxfile_config.py b/genai/tools/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/tools/noxfile_config.py +++ b/genai/tools/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/tools/requirements-test.txt b/genai/tools/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/genai/tools/requirements-test.txt +++ b/genai/tools/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/genai/tools/requirements.txt b/genai/tools/requirements.txt index 13bc5aa5291..b82955a5518 100644 --- a/genai/tools/requirements.txt +++ b/genai/tools/requirements.txt @@ -1,3 +1,3 @@ -google-genai==1.27.0 +google-genai==1.45.0 # PIl is required for tools_code_execution_with_txt_img.py -pillow==11.1.0 +pillow==12.3.0; python_version >= "3.10" diff --git a/genai/tools/test_tools_examples.py b/genai/tools/test_tools_examples.py index 26e5eb8ff5d..95611393b00 100644 --- a/genai/tools/test_tools_examples.py +++ b/genai/tools/test_tools_examples.py @@ -24,56 +24,63 @@ import tools_enterprise_web_search_with_txt import tools_func_def_with_txt import tools_func_desc_with_txt +import tools_google_maps_coordinates_with_txt import tools_google_maps_with_txt +import tools_google_search_and_urlcontext_with_txt import tools_google_search_with_txt +import tools_urlcontext_with_txt import tools_vais_with_txt -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" def test_tools_code_exec_with_txt() -> None: - response = tools_code_exec_with_txt.generate_content() - assert response + assert tools_code_exec_with_txt.generate_content() def test_tools_code_exec_with_txt_local_img() -> None: - response = tools_code_exec_with_txt_local_img.generate_content() - assert response + assert tools_code_exec_with_txt_local_img.generate_content() def test_tools_enterprise_web_search_with_txt() -> None: - response = tools_enterprise_web_search_with_txt.generate_content() - assert response + assert tools_enterprise_web_search_with_txt.generate_content() def test_tools_func_def_with_txt() -> None: - response = tools_func_def_with_txt.generate_content() - assert response + assert tools_func_def_with_txt.generate_content() def test_tools_func_desc_with_txt() -> None: - response = tools_func_desc_with_txt.generate_content() - assert response + assert tools_func_desc_with_txt.generate_content() @pytest.mark.skip( reason="Google Maps Grounding allowlisting is not set up for the test project." ) def test_tools_google_maps_with_txt() -> None: - response = tools_google_maps_with_txt.generate_content() - assert response + assert tools_google_maps_with_txt.generate_content() def test_tools_google_search_with_txt() -> None: - response = tools_google_search_with_txt.generate_content() - assert response + assert tools_google_search_with_txt.generate_content() def test_tools_vais_with_txt() -> None: PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT") datastore = f"projects/{PROJECT_ID}/locations/global/collections/default_collection/dataStores/grounding-test-datastore" - response = tools_vais_with_txt.generate_content(datastore) - assert response + assert tools_vais_with_txt.generate_content(datastore) + + +def test_tools_google_maps_coordinates_with_txt() -> None: + assert tools_google_maps_coordinates_with_txt.generate_content() + + +def test_tools_urlcontext_with_txt() -> None: + assert tools_urlcontext_with_txt.generate_content() + + +def test_tools_google_search_and_urlcontext_with_txt() -> None: + assert tools_google_search_and_urlcontext_with_txt.generate_content() diff --git a/genai/tools/tools_code_exec_with_txt.py b/genai/tools/tools_code_exec_with_txt.py index a97cd913446..5c4deecd301 100644 --- a/genai/tools/tools_code_exec_with_txt.py +++ b/genai/tools/tools_code_exec_with_txt.py @@ -17,14 +17,14 @@ def generate_content() -> str: # [START googlegenaisdk_tools_code_exec_with_txt] from google import genai from google.genai.types import ( + GenerateContentConfig, HttpOptions, Tool, ToolCodeExecution, - GenerateContentConfig, ) client = genai.Client(http_options=HttpOptions(api_version="v1")) - model_id = "gemini-2.5-flash" + model_id = "gemini-3.5-flash" code_execution_tool = Tool(code_execution=ToolCodeExecution()) response = client.models.generate_content( diff --git a/genai/tools/tools_code_exec_with_txt_local_img.py b/genai/tools/tools_code_exec_with_txt_local_img.py index b58102afb39..10a64799fe0 100644 --- a/genai/tools/tools_code_exec_with_txt_local_img.py +++ b/genai/tools/tools_code_exec_with_txt_local_img.py @@ -17,7 +17,6 @@ def generate_content() -> GenerateContentResponse: # [START googlegenaisdk_tools_code_exec_with_txt_local_img] - from PIL import Image from google import genai from google.genai.types import ( GenerateContentConfig, @@ -25,6 +24,7 @@ def generate_content() -> GenerateContentResponse: Tool, ToolCodeExecution, ) + from PIL import Image client = genai.Client(http_options=HttpOptions(api_version="v1")) code_execution_tool = Tool(code_execution=ToolCodeExecution()) @@ -46,7 +46,7 @@ def generate_content() -> GenerateContentResponse: image_data = Image.open(image_file) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents=[image_data, prompt], config=GenerateContentConfig( tools=[code_execution_tool], diff --git a/genai/tools/tools_enterprise_web_search_with_txt.py b/genai/tools/tools_enterprise_web_search_with_txt.py index 429f58600a9..c84ef9e8311 100644 --- a/genai/tools/tools_enterprise_web_search_with_txt.py +++ b/genai/tools/tools_enterprise_web_search_with_txt.py @@ -26,7 +26,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="When is the next total solar eclipse in the United States?", config=GenerateContentConfig( tools=[ diff --git a/genai/tools/tools_func_def_with_txt.py b/genai/tools/tools_func_def_with_txt.py index 89327dcd0cc..98581a43fcc 100644 --- a/genai/tools/tools_func_def_with_txt.py +++ b/genai/tools/tools_func_def_with_txt.py @@ -34,7 +34,7 @@ def get_current_weather(location: str) -> str: return weather_map.get(location, "unknown") client = genai.Client(http_options=HttpOptions(api_version="v1")) - model_id = "gemini-2.5-flash" + model_id = "gemini-3.5-flash" response = client.models.generate_content( model=model_id, diff --git a/genai/tools/tools_func_desc_with_txt.py b/genai/tools/tools_func_desc_with_txt.py index 6d89ede0fae..16c35ce291d 100644 --- a/genai/tools/tools_func_desc_with_txt.py +++ b/genai/tools/tools_func_desc_with_txt.py @@ -24,7 +24,7 @@ def generate_content() -> str: ) client = genai.Client(http_options=HttpOptions(api_version="v1")) - model_id = "gemini-2.5-flash" + model_id = "gemini-3.5-flash" get_album_sales = FunctionDeclaration( name="get_album_sales", diff --git a/genai/tools/tools_google_maps_coordinates_with_txt.py b/genai/tools/tools_google_maps_coordinates_with_txt.py new file mode 100644 index 00000000000..35253798d6a --- /dev/null +++ b/genai/tools/tools_google_maps_coordinates_with_txt.py @@ -0,0 +1,59 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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() -> str: + # [START googlegenaisdk_tools_google_maps_coordinates_with_txt] + from google import genai + from google.genai.types import ( + GenerateContentConfig, + GoogleMaps, + HttpOptions, + LatLng, + RetrievalConfig, + Tool, + ToolConfig, + ) + + client = genai.Client(http_options=HttpOptions(api_version="v1")) + + response = client.models.generate_content( + model="gemini-3.5-flash", + contents="Where can I get the best espresso near me?", + config=GenerateContentConfig( + tools=[ + # Use Google Maps Tool + Tool(google_maps=GoogleMaps()) + ], + tool_config=ToolConfig( + retrieval_config=RetrievalConfig( + lat_lng=LatLng( # Pass coordinates for location-aware grounding + latitude=40.7128, + longitude=-74.006 + ), + language_code="en_US", # Optional: localize Maps results + ), + ), + ), + ) + + print(response.text) + # Example response: + # 'Here are some of the top-rated places to get espresso near you: ...' + # [END googlegenaisdk_tools_google_maps_coordinates_with_txt] + return response.text + + +if __name__ == "__main__": + generate_content() diff --git a/genai/tools/tools_google_maps_with_txt.py b/genai/tools/tools_google_maps_with_txt.py index e2ff93e63b7..c9c3bbf336b 100644 --- a/genai/tools/tools_google_maps_with_txt.py +++ b/genai/tools/tools_google_maps_with_txt.py @@ -31,7 +31,7 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="Recommend a good restaurant in San Francisco.", config=GenerateContentConfig( tools=[ diff --git a/genai/tools/tools_google_search_and_urlcontext_with_txt.py b/genai/tools/tools_google_search_and_urlcontext_with_txt.py new file mode 100644 index 00000000000..bb3c9ba826f --- /dev/null +++ b/genai/tools/tools_google_search_and_urlcontext_with_txt.py @@ -0,0 +1,101 @@ +# 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 generate_content() -> str: + # [START googlegenaisdk_tools_google_search_and_urlcontext_with_txt] + from google import genai + from google.genai.types import ( + GenerateContentConfig, + GoogleSearch, + HttpOptions, + Tool, + UrlContext, + ) + + client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) + model_id = "gemini-3.5-flash" + + tools = [ + Tool(url_context=UrlContext), + Tool(google_search=GoogleSearch), + ] + + # TODO(developer): Here put your URLs! + url = 'https://www.google.com/search?q=events+in+New+York' + + response = client.models.generate_content( + model=model_id, + contents=f"Give me three day events schedule based on {url}. Also let me know what needs to taken care of considering weather and commute.", + config=GenerateContentConfig( + tools=tools, + response_modalities=["TEXT"], + ) + ) + + for each in response.candidates[0].content.parts: + print(each.text) + # Here is a possible three-day event schedule for New York City, focusing on the dates around October 7-9, 2025, along with weather and commute considerations. + # + # ### Three-Day Event Schedule: New York City (October 7-9, 2025) + # + # **Day 1: Tuesday, October 7, 2025 - Art and Culture** + # + # * **Morning (10:00 AM - 1:00 PM):** Visit "Phillips Visual Language: The Art of Irving Penn" at 432 Park Avenue. This exhibition is scheduled to end on this day, offering a last chance to see it. + # * **Lunch (1:00 PM - 2:00 PM):** Grab a quick lunch near Park Avenue. + # * **Afternoon (2:30 PM - 5:30 PM):** Explore the "Lincoln Center Festival of Firsts" at Lincoln Center. This festival runs until October 23rd, offering various performances or exhibits. Check their specific schedule for the day. + # * **Evening (7:00 PM onwards):** Experience a classic Broadway show. Popular options mentioned for October 2025 include "Six The Musical," "Wicked," "Hadestown," or "MJ - The Musical." + # + # **Day 2: Wednesday, October 8, 2025 - Unique Experiences and SoHo Vibes** + # + # * **Morning (11:00 AM - 1:00 PM):** Head to Brooklyn for the "Secret Room at IKEA Brooklyn" at 1 Beard Street. This unique event is scheduled to end on October 9th. + # * **Lunch (1:00 PM - 2:00 PM):** Enjoy lunch in Brooklyn, perhaps exploring local eateries in the area. + # * **Afternoon (2:30 PM - 5:30 PM):** Immerse yourself in the "The Weeknd & Nespresso Samra Origins Vinyl Cafe" at 579 Broadway in SoHo. This pop-up, curated by The Weeknd, combines coffee and music and runs until October 14th. + # * **Evening (6:00 PM onwards):** Explore the vibrant SoHo neighborhood, known for its shopping and dining. You could also consider a dinner cruise to see the illuminated Manhattan skyline and the Statue of Liberty. + # + # **Day 3: Thursday, October 9, 2025 - Film and Scenic Views** + # + # * **Morning (10:00 AM - 1:00 PM):** Attend a screening at the New York Greek Film Expo, which runs until October 12th in New York City. + # * **Lunch (1:00 PM - 2:00 PM):** Have lunch near the film expo's location. + # * **Afternoon (2:30 PM - 5:30 PM):** Take advantage of the pleasant October weather and enjoy outdoor activities. Consider biking along the rivers or through Central Park to admire the early autumn foliage. + # * **Evening (6:00 PM onwards):** Visit an observation deck like the Empire State Building or Top of the Rock for panoramic city views. Afterwards, enjoy dinner in a neighborhood of your choice. + # + # ### Weather and Commute Considerations: + # + # **Weather in Early October:** + # + # * **Temperatures:** Expect mild to cool temperatures. Average daily temperatures in early October range from 10°C (50°F) to 18°C (64°F), with occasional warmer days reaching the mid-20s°C (mid-70s°F). Evenings can be quite chilly. + # * **Rainfall:** October has a higher chance of rainfall compared to other months, with an average of 33mm and a 32% chance of rain on any given day. + # * **Sunshine:** You can generally expect about 7 hours of sunshine per day. + # * **What to Pack:** Pack layers! Bring a light jacket or sweater for the daytime, and a warmer coat for the evenings. An umbrella or a light raincoat is highly recommended due to the chance of showers. Comfortable walking shoes are a must for exploring the city. + # + # **Commute in New York City:** + # + # * **Public Transportation is Key:** The subway is generally the fastest and most efficient way to get around New York City, especially during the day. Buses are good for East-West travel, but can be slower due to traffic. + # * **Using Apps:** Utilize Google Maps or official MTA apps to plan your routes and check for real-time service updates. The subway runs 24/7, but expect potential delays or changes to routes during nights and weekends due to maintenance. + # * **Rush Hour:** Avoid subway and commuter train travel during peak rush hours (8 AM - 10 AM and 5 PM - 7 PM) if possible, as trains can be extremely crowded. + # * **Subway Etiquette:** When on the subway, stand to the side of the doors to let people exit before boarding, and move to the center of the car to make space. Hold onto a pole or seat, and remove your backpack to free up space. + # * **Transfers:** Subway fare is $2.90 per ride, and you get one free transfer between the subway and bus within a two-hour window. + # * **Walking:** New York City is very walkable. If the weather is pleasant, walking between nearby attractions is an excellent way to see the city. + # * **Taxis/Ride-sharing:** Uber, Lyft, and Curb (for NYC taxis) are available, but driving in the city is generally discouraged due to traffic and parking difficulties. + # * **Allow Extra Time:** Always factor in an additional 20-30 minutes for travel time, as delays can occur. + + # get URLs retrieved for context + print(response.candidates[0].url_context_metadata) + # [END googlegenaisdk_tools_google_search_and_urlcontext_with_txt] + return response.text + + +if __name__ == "__main__": + generate_content() diff --git a/genai/tools/tools_google_search_with_txt.py b/genai/tools/tools_google_search_with_txt.py index 2f650b01df9..fe1fe89030c 100644 --- a/genai/tools/tools_google_search_with_txt.py +++ b/genai/tools/tools_google_search_with_txt.py @@ -26,12 +26,17 @@ def generate_content() -> str: client = genai.Client(http_options=HttpOptions(api_version="v1")) response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="When is the next total solar eclipse in the United States?", 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/tools/tools_urlcontext_with_txt.py b/genai/tools/tools_urlcontext_with_txt.py new file mode 100644 index 00000000000..d64d3e1051d --- /dev/null +++ b/genai/tools/tools_urlcontext_with_txt.py @@ -0,0 +1,85 @@ +# 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 generate_content() -> str: + # [START googlegenaisdk_tools_urlcontext_with_txt] + from google import genai + from google.genai.types import GenerateContentConfig, HttpOptions, Tool, UrlContext + + client = genai.Client(http_options=HttpOptions(api_version="v1")) + model_id = "gemini-3.5-flash" + + url_context_tool = Tool( + url_context=UrlContext + ) + + # TODO(developer): Here put your URLs + url1 = "https://cloud.google.com/vertex-ai/docs/generative-ai/start" + url2 = "https://cloud.google.com/docs/overview" + + response = client.models.generate_content( + model=model_id, + contents=f"Compare the content, purpose, and audiences of {url1} and {url2}.", + config=GenerateContentConfig( + tools=[url_context_tool], + response_modalities=["TEXT"], + ) + ) + + for each in response.candidates[0].content.parts: + print(each.text) + # Gemini 2.5 Pro and Gemini 2.5 Flash are both advanced models offered by Google AI, but they are optimized for different use cases. + # + # Here's a comparison: + # + # **Gemini 2.5 Pro** + # * **Description**: This is Google's most advanced model, described as a "state-of-the-art thinking model". It excels at reasoning over complex problems in areas like code, mathematics, and STEM, and can analyze large datasets, codebases, and documents using a long context window. + # * **Input Data Types**: It supports audio, images, video, text, and PDF inputs. + # * **Output Data Types**: It produces text outputs. + # * **Token Limits**: It has an input token limit of 1,048,576 and an output token limit of 65,536. + # * **Supported Capabilities**: Gemini 2.5 Pro supports Batch API, Caching, Code execution, Function calling, Search grounding, Structured outputs, Thinking, and URL context. + # * **Knowledge Cutoff**: January 2025. + # + # **Gemini 2.5 Flash** + # * **Description**: Positioned as "fast and intelligent," Gemini 2.5 Flash is highlighted as Google's best model in terms of price-performance, offering well-rounded capabilities. It is ideal for large-scale processing, low-latency, high-volume tasks that require thinking, and agentic use cases. + # * **Input Data Types**: It supports text, images, video, and audio inputs. + # * **Output Data Types**: It produces text outputs. + # * **Token Limits**: Similar to Pro, it has an input token limit of 1,048,576 and an output token limit of 65,536. + # * **Supported Capabilities**: Gemini 2.5 Flash supports Batch API, Caching, Code execution, Function calling, Search grounding, Structured outputs, Thinking, and URL context. + # * **Knowledge Cutoff**: January 2025. + # + # **Key Differences and Similarities:** + # + # * **Primary Focus**: Gemini 2.5 Pro is geared towards advanced reasoning and in-depth analysis of complex problems and large documents. Gemini 2.5 Flash, on the other hand, is optimized for efficiency, scale, and high-volume, low-latency applications, making it a strong choice for price-performance sensitive scenarios. + # * **Input Modalities**: Both models handle various input types including text, images, video, and audio. Gemini 2.5 Pro explicitly lists PDF as an input type, while Gemini 2.5 Flash lists text, images, video, audio. + # * **Technical Specifications (for primary stable versions)**: Both models share the same substantial input and output token limits (1,048,576 input and 65,536 output). They also support a very similar set of core capabilities, including code execution, function calling, and URL context. Neither model supports audio generation, image generation, or Live API in their standard stable versions. + # * **Knowledge Cutoff**: Both models have a knowledge cutoff of January 2025. + # + # In essence, while both models are powerful and capable, Gemini 2.5 Pro is designed for maximum performance in complex reasoning tasks, whereas Gemini 2.5 Flash prioritizes cost-effectiveness and speed for broader, high-throughput applications. + # get URLs retrieved for context + print(response.candidates[0].url_context_metadata) + # url_metadata=[UrlMetadata( + # retrieved_url='https://ai.google.dev/gemini-api/docs/models#gemini-3.5-flash', + # url_retrieval_status= + # ), UrlMetadata( + # retrieved_url='https://ai.google.dev/gemini-api/docs/models#gemini-3.1-pro-preview', + # url_retrieval_status= + # )] + # [END googlegenaisdk_tools_urlcontext_with_txt] + return response.text + + +if __name__ == "__main__": + generate_content() diff --git a/genai/tools/tools_vais_with_txt.py b/genai/tools/tools_vais_with_txt.py index fa4109d5979..e018cbcd20c 100644 --- a/genai/tools/tools_vais_with_txt.py +++ b/genai/tools/tools_vais_with_txt.py @@ -30,7 +30,7 @@ def generate_content(datastore: str) -> str: # datastore = "projects/111111111111/locations/global/collections/default_collection/dataStores/data-store-id" response = client.models.generate_content( - model="gemini-2.5-flash", + model="gemini-3.5-flash", contents="How do I make an appointment to renew my driver's license?", config=GenerateContentConfig( tools=[ @@ -50,7 +50,7 @@ def generate_content(datastore: str) -> str: # Example response: # 'The process for making an appointment to renew your driver's license varies depending on your location. To provide you with the most accurate instructions...' # [END googlegenaisdk_tools_vais_with_txt] - return response.text + return True if __name__ == "__main__": diff --git a/genai/tuning/noxfile_config.py b/genai/tuning/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/tuning/noxfile_config.py +++ b/genai/tuning/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/tuning/preference_tuning_job_create.py b/genai/tuning/preference_tuning_job_create.py new file mode 100644 index 00000000000..04a77958a05 --- /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 CreateTuningJobConfig, HttpOptions, 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-3.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-test.txt b/genai/tuning/requirements-test.txt index 4ccc4347cbe..14a8493498b 100644 --- a/genai/tuning/requirements-test.txt +++ b/genai/tuning/requirements-test.txt @@ -1,3 +1,3 @@ google-api-core==2.24.0 google-cloud-storage==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/tuning/requirements.txt b/genai/tuning/requirements.txt index d64aa5a57b1..e5fdb322ca4 100644 --- a/genai/tuning/requirements.txt +++ b/genai/tuning/requirements.txt @@ -1 +1 @@ -google-genai==1.30.0 +google-genai==1.47.0 diff --git a/genai/tuning/test_tuning_examples.py b/genai/tuning/test_tuning_examples.py index 0974c769483..baf841623ac 100644 --- a/genai/tuning/test_tuning_examples.py +++ b/genai/tuning/test_tuning_examples.py @@ -13,13 +13,14 @@ # limitations under the License. from datetime import datetime as dt - from unittest.mock import call, MagicMock, patch from google.cloud import storage from google.genai import types -import pytest +import preference_tuning_job_create +import pytest +import tuning_job_cancel import tuning_job_create import tuning_job_get import tuning_job_list @@ -29,6 +30,7 @@ import tuning_with_checkpoints_list_checkpoints import tuning_with_checkpoints_set_default_checkpoint import tuning_with_checkpoints_textgen_with_txt +import tuning_with_pretuned_model GCS_OUTPUT_BUCKET = "python-docs-samples-tests" @@ -87,6 +89,14 @@ def test_tuning_job_get(mock_genai_client: MagicMock) -> None: assert response == "test-tuning-job" +@patch("google.genai.Client") +def test_tuning_job_cancel(mock_genai_client: MagicMock) -> None: + tuning_job_cancel.cancel_tuning_job("test-tuning-job") + + mock_genai_client.assert_called_once_with(http_options=types.HttpOptions(api_version="v1")) + mock_genai_client.return_value.tunings.cancel.assert_called_once_with(name="test-tuning-job") + + @patch("google.genai.Client") def test_tuning_job_list(mock_genai_client: MagicMock) -> None: # Mock the API response @@ -306,3 +316,43 @@ def test_tuning_with_checkpoints_textgen_with_txt(mock_genai_client: MagicMock) call(model="test-endpoint-1", contents="Why is the sky blue?"), call(model="test-endpoint-2", contents="Why is the sky blue?"), ] + + +@patch("google.genai.Client") +def test_tuning_with_pretuned_model(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-2", + endpoint="test-endpoint" + ) + ) + mock_genai_client.return_value.tunings.tune.return_value = mock_tuning_job + + response = tuning_with_pretuned_model.create_continuous_tuning_job(tuned_model_name="test-model", checkpoint_id="1") + + 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/tuning/tuning_job_cancel.py b/genai/tuning/tuning_job_cancel.py new file mode 100644 index 00000000000..708f63a5011 --- /dev/null +++ b/genai/tuning/tuning_job_cancel.py @@ -0,0 +1,29 @@ +# 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. + +# [START googlegenaisdk_tuning_job_cancel] + +from google import genai +from google.genai.types import HttpOptions + + +def cancel_tuning_job(tuning_job_name: str) -> None: + client = genai.Client(http_options=HttpOptions(api_version="v1")) + + # Cancel the tuning job. + # Eg. tuning_job_name = "projects/123456789012/locations/us-central1/tuningJobs/123456789012345" + client.tunings.cancel(name=tuning_job_name) + + +# [END googlegenaisdk_tuning_job_cancel] diff --git a/genai/tuning/tuning_job_create.py b/genai/tuning/tuning_job_create.py index 168b8a50c3b..a73c444f73b 100644 --- a/genai/tuning/tuning_job_create.py +++ b/genai/tuning/tuning_job_create.py @@ -18,7 +18,15 @@ def create_tuning_job(output_gcs_uri: str) -> str: import time from google import genai - from google.genai.types import HttpOptions, CreateTuningJobConfig, TuningDataset, EvaluationConfig, OutputConfig, GcsDestination, Metric + from google.genai.types import ( + CreateTuningJobConfig, + EvaluationConfig, + GcsDestination, + HttpOptions, + Metric, + OutputConfig, + TuningDataset, + ) # TODO(developer): Update and un-comment below line # output_gcs_uri = "gs://your-bucket/your-prefix" @@ -47,7 +55,7 @@ def create_tuning_job(output_gcs_uri: str) -> str: ) tuning_job = client.tunings.tune( - base_model="gemini-2.5-flash", + base_model="gemini-3.5-flash", training_dataset=training_dataset, config=CreateTuningJobConfig( tuned_model_display_name="Example tuning job", diff --git a/genai/tuning/tuning_with_checkpoints_create.py b/genai/tuning/tuning_with_checkpoints_create.py index d15db2bc819..0938dd52264 100644 --- a/genai/tuning/tuning_with_checkpoints_create.py +++ b/genai/tuning/tuning_with_checkpoints_create.py @@ -18,7 +18,15 @@ def create_with_checkpoints(output_gcs_uri: str) -> str: import time from google import genai - from google.genai.types import HttpOptions, CreateTuningJobConfig, TuningDataset, EvaluationConfig, OutputConfig, GcsDestination, Metric + from google.genai.types import ( + CreateTuningJobConfig, + EvaluationConfig, + GcsDestination, + HttpOptions, + Metric, + OutputConfig, + TuningDataset, + ) # TODO(developer): Update and un-comment below line # output_gcs_uri = "gs://your-bucket/your-prefix" @@ -47,7 +55,7 @@ def create_with_checkpoints(output_gcs_uri: str) -> str: ) tuning_job = client.tunings.tune( - base_model="gemini-2.5-flash", + base_model="gemini-3.5-flash", training_dataset=training_dataset, config=CreateTuningJobConfig( tuned_model_display_name="Example tuning job", diff --git a/genai/tuning/tuning_with_pretuned_model.py b/genai/tuning/tuning_with_pretuned_model.py new file mode 100644 index 00000000000..75911b51206 --- /dev/null +++ b/genai/tuning/tuning_with_pretuned_model.py @@ -0,0 +1,78 @@ +# 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_continuous_tuning_job(tuned_model_name: str, checkpoint_id: str) -> str: + # [START googlegenaisdk_tuning_with_pretuned_model] + import time + + from google import genai + from google.genai.types import HttpOptions, TuningDataset, CreateTuningJobConfig + + # TODO(developer): Update and un-comment below line + # tuned_model_name = "projects/123456789012/locations/us-central1/models/1234567890@1" + # checkpoint_id = "1" + + client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) + + training_dataset = TuningDataset( + gcs_uri="gs://cloud-samples-data/ai-platform/generative_ai/gemini/text/sft_train_data.jsonl", + ) + validation_dataset = TuningDataset( + gcs_uri="gs://cloud-samples-data/ai-platform/generative_ai/gemini/text/sft_validation_data.jsonl", + ) + + tuning_job = client.tunings.tune( + base_model=tuned_model_name, # Note: Using a Tuned Model + training_dataset=training_dataset, + config=CreateTuningJobConfig( + tuned_model_display_name="Example tuning job", + validation_dataset=validation_dataset, + pre_tuned_model_checkpoint_id=checkpoint_id, + ), + ) + + 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@2 + # 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_tuning_with_pretuned_model] + return tuning_job.name + + +if __name__ == "__main__": + pre_tuned_model_name = input("Pre-tuned model name: ") + pre_tuned_model_checkpoint_id = input("Pre-tuned model checkpoint id: ") + create_continuous_tuning_job(pre_tuned_model_name, pre_tuned_model_checkpoint_id) diff --git a/genai/video_generation/noxfile_config.py b/genai/video_generation/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/genai/video_generation/noxfile_config.py +++ b/genai/video_generation/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/genai/video_generation/requirements-test.txt b/genai/video_generation/requirements-test.txt index 4ccc4347cbe..14a8493498b 100644 --- a/genai/video_generation/requirements-test.txt +++ b/genai/video_generation/requirements-test.txt @@ -1,3 +1,3 @@ google-api-core==2.24.0 google-cloud-storage==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/genai/video_generation/requirements.txt b/genai/video_generation/requirements.txt index 7ad844cebcf..b83c25fae61 100644 --- a/genai/video_generation/requirements.txt +++ b/genai/video_generation/requirements.txt @@ -1 +1 @@ -google-genai==1.27.0 +google-genai==1.43.0 diff --git a/genai/video_generation/test_video_generation_examples.py b/genai/video_generation/test_video_generation_examples.py index 15f060e7674..079bdf7e97c 100644 --- a/genai/video_generation/test_video_generation_examples.py +++ b/genai/video_generation/test_video_generation_examples.py @@ -30,12 +30,18 @@ import videogen_with_no_rewrite +import videogen_with_reference + import videogen_with_txt import videogen_with_vid +import videogen_with_vid_edit_insert + +import videogen_with_vid_edit_remove + -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" @@ -79,3 +85,18 @@ def test_videogen_with_vid(output_gcs_uri: str) -> None: def test_videogen_with_no_rewriter(output_gcs_uri: str) -> None: response = videogen_with_no_rewrite.generate_videos_no_rewriter(output_gcs_uri=output_gcs_uri) assert response + + +def test_videogen_with_reference(output_gcs_uri: str) -> None: + response = videogen_with_reference.generate_videos_from_reference(output_gcs_uri=output_gcs_uri) + assert response + + +def test_videogen_with_edit_insert(output_gcs_uri: str) -> None: + response = videogen_with_vid_edit_insert.edit_videos_insert_from_video(output_gcs_uri=output_gcs_uri) + assert response + + +def test_videogen_with_edit_remove(output_gcs_uri: str) -> None: + response = videogen_with_vid_edit_remove.edit_videos_remove_from_video(output_gcs_uri=output_gcs_uri) + assert response diff --git a/genai/video_generation/videogen_with_first_last_frame.py b/genai/video_generation/videogen_with_first_last_frame.py index 026cd545d87..52b5ab3a58a 100644 --- a/genai/video_generation/videogen_with_first_last_frame.py +++ b/genai/video_generation/videogen_with_first_last_frame.py @@ -25,7 +25,7 @@ def generate_videos_from_first_last_frame(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-001", prompt="a hand reaches in and places a glass of milk next to the plate of cookies", image=Image( gcs_uri="gs://cloud-samples-data/generative-ai/image/cookies.png", diff --git a/genai/video_generation/videogen_with_img.py b/genai/video_generation/videogen_with_img.py index fe8e1f48305..ce725b1b03c 100644 --- a/genai/video_generation/videogen_with_img.py +++ b/genai/video_generation/videogen_with_img.py @@ -25,7 +25,7 @@ def generate_videos_from_image(output_gcs_uri: str) -> str: # output_gcs_uri = "gs://your-bucket/your-prefix" operation = client.models.generate_videos( - model="veo-3.0-generate-preview", + model="veo-3.1-generate-001", prompt="Extreme close-up of a cluster of vibrant wildflowers swaying gently in a sun-drenched meadow.", image=Image( gcs_uri="gs://cloud-samples-data/generative-ai/image/flowers.png", diff --git a/genai/video_generation/videogen_with_reference.py b/genai/video_generation/videogen_with_reference.py new file mode 100644 index 00000000000..6543530ff9d --- /dev/null +++ b/genai/video_generation/videogen_with_reference.py @@ -0,0 +1,60 @@ +# 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 generate_videos_from_reference(output_gcs_uri: str) -> str: + # [START googlegenaisdk_videogen_with_img_reference] + import time + from google import genai + from google.genai.types import GenerateVideosConfig, Image, VideoGenerationReferenceImage + + client = genai.Client() + + # TODO(developer): Update and un-comment below line + # output_gcs_uri = "gs://your-bucket/your-prefix" + + operation = client.models.generate_videos( + model="veo-3.1-generate-preview", + 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/vase.png", + mime_type="image/png", + ), + reference_type="asset", + ), + ], + aspect_ratio="9:16", + output_gcs_uri=output_gcs_uri, + ), + ) + + while not operation.done: + time.sleep(15) + operation = client.operations.get(operation) + print(operation) + + if operation.response: + print(operation.result.generated_videos[0].video.uri) + + # Example response: + # gs://your-bucket/your-prefix + # [END googlegenaisdk_videogen_with_img_reference] + return operation.result.generated_videos[0].video.uri + + +if __name__ == "__main__": + generate_videos_from_reference(output_gcs_uri="gs://your-bucket/your-prefix") diff --git a/genai/video_generation/videogen_with_txt.py b/genai/video_generation/videogen_with_txt.py index a6d9934dc19..17ad11df4a3 100644 --- a/genai/video_generation/videogen_with_txt.py +++ b/genai/video_generation/videogen_with_txt.py @@ -25,7 +25,7 @@ def generate_videos(output_gcs_uri: str) -> str: # output_gcs_uri = "gs://your-bucket/your-prefix" operation = client.models.generate_videos( - model="veo-3.0-generate-001", + model="veo-3.1-generate-001", prompt="a cat reading a book", config=GenerateVideosConfig( aspect_ratio="16:9", diff --git a/genai/video_generation/videogen_with_vid.py b/genai/video_generation/videogen_with_vid.py index d8ff763e525..efcd63bcb4b 100644 --- a/genai/video_generation/videogen_with_vid.py +++ b/genai/video_generation/videogen_with_vid.py @@ -25,13 +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/genai/video_generation/videogen_with_vid_edit_insert.py b/genai/video_generation/videogen_with_vid_edit_insert.py new file mode 100644 index 00000000000..e45b1da5863 --- /dev/null +++ b/genai/video_generation/videogen_with_vid_edit_insert.py @@ -0,0 +1,60 @@ +# 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 edit_videos_insert_from_video(output_gcs_uri: str) -> str: + # [START googlegenaisdk_videogen_with_vid_edit_insert] + import time + from google import genai + from google.genai.types import GenerateVideosSource, GenerateVideosConfig, Image, Video, VideoGenerationMask, VideoGenerationMaskMode + + client = genai.Client() + + # TODO(developer): Update and un-comment below line + # output_gcs_uri = "gs://your-bucket/your-prefix" + + operation = client.models.generate_videos( + model="veo-2.0-generate-preview", + source=GenerateVideosSource( + prompt="a sheep", + video=Video(uri="gs://cloud-samples-data/generative-ai/video/truck.mp4", mime_type="video/mp4") + ), + config=GenerateVideosConfig( + mask=VideoGenerationMask( + image=Image( + gcs_uri="gs://cloud-samples-data/generative-ai/image/truck-inpainting-dynamic-mask.png", + mime_type="image/png", + ), + mask_mode=VideoGenerationMaskMode.INSERT, + ), + output_gcs_uri=output_gcs_uri, + ), + ) + + while not operation.done: + time.sleep(15) + operation = client.operations.get(operation) + print(operation) + + if operation.response: + print(operation.result.generated_videos[0].video.uri) + + # Example response: + # gs://your-bucket/your-prefix + # [END googlegenaisdk_videogen_with_vid_edit_insert] + return operation.result.generated_videos[0].video.uri + + +if __name__ == "__main__": + edit_videos_insert_from_video(output_gcs_uri="gs://your-bucket/your-prefix") diff --git a/genai/video_generation/videogen_with_vid_edit_remove.py b/genai/video_generation/videogen_with_vid_edit_remove.py new file mode 100644 index 00000000000..ef0cd5cd2cc --- /dev/null +++ b/genai/video_generation/videogen_with_vid_edit_remove.py @@ -0,0 +1,59 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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 edit_videos_remove_from_video(output_gcs_uri: str) -> str: + # [START googlegenaisdk_videogen_with_vid_edit_remove] + import time + from google import genai + from google.genai.types import GenerateVideosSource, GenerateVideosConfig, Image, Video, VideoGenerationMask, VideoGenerationMaskMode + + client = genai.Client() + + # TODO(developer): Update and un-comment below line + # output_gcs_uri = "gs://your-bucket/your-prefix" + + operation = client.models.generate_videos( + model="veo-2.0-generate-preview", + source=GenerateVideosSource( + video=Video(uri="gs://cloud-samples-data/generative-ai/video/truck.mp4", mime_type="video/mp4") + ), + config=GenerateVideosConfig( + mask=VideoGenerationMask( + image=Image( + gcs_uri="gs://cloud-samples-data/generative-ai/image/truck-inpainting-dynamic-mask.png", + mime_type="image/png", + ), + mask_mode=VideoGenerationMaskMode.REMOVE, + ), + output_gcs_uri=output_gcs_uri, + ), + ) + + while not operation.done: + time.sleep(15) + operation = client.operations.get(operation) + print(operation) + + if operation.response: + print(operation.result.generated_videos[0].video.uri) + + # Example response: + # gs://your-bucket/your-prefix + # [END googlegenaisdk_videogen_with_vid_edit_remove] + return operation.result.generated_videos[0].video.uri + + +if __name__ == "__main__": + edit_videos_remove_from_video(output_gcs_uri="gs://your-bucket/your-prefix") diff --git a/generative_ai/README.md b/generative_ai/README.md index 9cd7509813b..caf5c1ea389 100644 --- a/generative_ai/README.md +++ b/generative_ai/README.md @@ -1,15 +1,14 @@ -# Generative AI Samples on Google Cloud +# Generative AI on Google Cloud: Python Samples -Welcome to the Python samples folder for Generative AI on Vertex AI! In this folder, you can find the Python samples -used in [Google Cloud Generative AI documentation](https://cloud.google.com/ai/generative-ai?hl=en). +This directory contains the official Python code samples featured in the [Google Cloud Generative AI documentation](https://cloud.google.com/ai/generative-ai?hl=en). These scripts demonstrate how to integrate and build with Vertex AI. -If you are looking for colab notebook, then this [link](https://github.com/GoogleCloudPlatform/generative-ai/tree/main). +Looking for interactive, step-by-step tutorials? Check out our extensive collection of [Colab notebooks](https://github.com/GoogleCloudPlatform/generative-ai/tree/main). ## Getting Started -To try and run these Code samples, we have following recommend using Google Cloud IDE or Google Colab. +> **Note:** An active Google Cloud Project is required. -Note: A Google Cloud Project is a pre-requisite. +We recommend running these code samples using Google Cloud Shell Editor or Google Colab to minimize environment setup. ### Feature folders @@ -21,47 +20,26 @@ Browse the folders below to find the Generative AI capabilities you're intereste Google Cloud Product - Short Description (With the help of Gemini 1.5) - - - - Context Caching - - https://cloud.google.com/vertex-ai/generative-ai/docs/context-cache/context-cache-overview - - Code samples demonstrating how to use context caching with Vertex AI's generative models. This allows for more consistent and relevant responses across multiple interactions by storing previous conversation history. - - - - Controlled Generation - - https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output - - Examples of how to control the output of generative models, such as specifying length, format, or sentiment. - - - - Count Token - - https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/list-token - - Code demonstrating how to count tokens in text, which is crucial for managing costs and understanding model limitations. + Short Description (With the help of Gemini 3.1) + Embeddings https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings - Code showing how to generate and use embeddings from text or images. Embeddings can be used for tasks like semantic search, clustering, and classification. + Learn how to use Vertex AI's text and multimodal embedding models. These samples show you how to convert your unstructured data into numerical vectors to power semantic search, clustering, and RAG applications. + + Extensions https://cloud.google.com/vertex-ai/generative-ai/docs/extensions/overview - Demonstrations of how to use extensions with generative models, enabling them to access and process real-time information, use tools, and interact with external systems. + These samples show how to connect Gemini to external APIs and databases so your models can retrieve live data and execute real-world actions. **Note** that as Google Cloud transitions to the Gemini Enterprise Agent Platform, standalone Vertex AI Extensions are evolving into *Tools* managed within the centralized Agent Registry. While these examples teach the core mechanics of model-to-API communication, future production applications should adopt the new Agent Platform architecture.. @@ -69,23 +47,16 @@ Browse the folders below to find the Generative AI capabilities you're intereste https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling - Examples of how to use function calling to enable generative models to execute specific actions or retrieve information from external APIs. - - - - Grounding - - https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/overview - - Code illustrating how to ground generative models with specific knowledge bases or data sources to improve the accuracy and relevance of their responses. + Function calling gives Gemini the ability to interact with your codebase. The model predicts which of your local functions needs to be run and returns the formatted arguments, leaving the actual execution up to your application. + Image Generation https://cloud.google.com/vertex-ai/generative-ai/docs/image/overview - Samples showcasing how to generate images from text prompts using models like Imagen. + Learn how to integrate the Imagen model into your applications. These examples cover text-to-image generation, editing, and using advanced parameters to get the exact visual output you need. @@ -93,7 +64,7 @@ Browse the folders below to find the Generative AI capabilities you're intereste https://cloud.google.com/vertex-ai/generative-ai/docs/model-garden/explore-models - Resources related to exploring and utilizing pre-trained models available in Vertex AI's Model Garden. + These examples show you how to provision endpoints and serve predictions from first-party, third-party, and open-source foundation models available in the Vertex AI Model Garden. @@ -101,7 +72,7 @@ Browse the folders below to find the Generative AI capabilities you're intereste https://cloud.google.com/vertex-ai/generative-ai/docs/models/tune-models - Code and guides for fine-tuning pre-trained generative models on specific datasets or for specific tasks. + Tailor Gemini and other foundation models to your specific domain. These examples cover how to format your datasets, kick off tuning jobs on Vertex AI, and deploy your custom-tuned models or adapters to production endpoints. @@ -109,7 +80,7 @@ Browse the folders below to find the Generative AI capabilities you're intereste https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/rag-api - Information and resources about Retrieval Augmented Generation (RAG), which combines information retrieval with generative models. + These examples cover the end-to-end RAG architecture: ingesting data, generating embeddings, querying a vector database, and passing the retrieved context to Gemini to generate informed, accurate answers. @@ -117,23 +88,7 @@ Browse the folders below to find the Generative AI capabilities you're intereste https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/reasoning-engine - Details about the Reasoning Engine, which enables more complex reasoning and logical deduction in generative models. - - - - Safety - - https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-attributes - - Examples of how to configure safety attributes and filters to mitigate risks and ensure responsible use of generative models. - - - - System Instructions - - https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/system-instructions?hl=en - - Code demonstrating how to provide system instructions to guide the behavior and responses of generative models. + These examples cover how to use Vertex AI Reasoning Engine to build custom agents. @@ -141,23 +96,7 @@ Browse the folders below to find the Generative AI capabilities you're intereste https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-chat-prompts-gemini - Samples of how to generate text using Gemini models, including chat-based interactions and creative writing. - - - - Understand Audio - - https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/audio-understanding - - Examples of how to use generative models for audio understanding tasks, such as transcription and audio classification. - - - - Understand Video - - https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/video-understanding - - Samples showcasing how to use generative models for video understanding tasks, such as video summarization and content analysis. + These samples demonstrate how to use Vertex AI's Gemini models to generate, summarize, and extract information from text. diff --git a/generative_ai/chat_completions/noxfile_config.py b/generative_ai/chat_completions/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/chat_completions/noxfile_config.py +++ b/generative_ai/chat_completions/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/chat_completions/requirements-test.txt b/generative_ai/chat_completions/requirements-test.txt index 3b9949d8513..3a0f817634b 100644 --- a/generative_ai/chat_completions/requirements-test.txt +++ b/generative_ai/chat_completions/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/embeddings/code_retrieval_example.py b/generative_ai/embeddings/code_retrieval_example.py deleted file mode 100644 index 4bd88fa9366..00000000000 --- a/generative_ai/embeddings/code_retrieval_example.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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. - -from __future__ import annotations - -# [START generativeaionvertexai_embedding_code_retrieval] -from vertexai.language_models import TextEmbeddingInput, TextEmbeddingModel - -MODEL_NAME = "gemini-embedding-001" -DIMENSIONALITY = 3072 - - -def embed_text( - texts: list[str] = ["Retrieve a function that adds two numbers"], - task: str = "CODE_RETRIEVAL_QUERY", - model_name: str = "gemini-embedding-001", - dimensionality: int | None = 3072, -) -> list[list[float]]: - """Embeds texts with a pre-trained, foundational model.""" - model = TextEmbeddingModel.from_pretrained(model_name) - kwargs = dict(output_dimensionality=dimensionality) if dimensionality else {} - - embeddings = [] - # gemini-embedding-001 takes one input at a time - for text in texts: - text_input = TextEmbeddingInput(text, task) - embedding = model.get_embeddings([text_input], **kwargs) - print(embedding) - # Example response: - # [[0.006135190837085247, -0.01462465338408947, 0.004978656303137541, ...]] - embeddings.append(embedding[0].values) - - return embeddings - - -if __name__ == "__main__": - # Embeds code block with a pre-trained, foundational model. - # Using this function to calculate the embedding for corpus. - texts = ["Retrieve a function that adds two numbers"] - task = "CODE_RETRIEVAL_QUERY" - code_block_embeddings = embed_text( - texts=texts, task=task, model_name=MODEL_NAME, dimensionality=DIMENSIONALITY - ) - - # Embeds code retrieval with a pre-trained, foundational model. - # Using this function to calculate the embedding for query. - texts = [ - "def func(a, b): return a + b", - "def func(a, b): return a - b", - "def func(a, b): return (a ** 2 + b ** 2) ** 0.5", - ] - task = "RETRIEVAL_DOCUMENT" - code_query_embeddings = embed_text( - texts=texts, task=task, model_name=MODEL_NAME, dimensionality=DIMENSIONALITY - ) - -# [END generativeaionvertexai_embedding_code_retrieval] diff --git a/generative_ai/embeddings/document_retrieval_example.py b/generative_ai/embeddings/document_retrieval_example.py deleted file mode 100644 index 71e9d6e0a0c..00000000000 --- a/generative_ai/embeddings/document_retrieval_example.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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. - -# [START generativeaionvertexai_embedding] -from __future__ import annotations - -from vertexai.language_models import TextEmbeddingInput, TextEmbeddingModel - - -def embed_text() -> list[list[float]]: - """Embeds texts with a pre-trained, foundational model. - - Returns: - A list of lists containing the embedding vectors for each input text - """ - - # A list of texts to be embedded. - texts = ["banana muffins? ", "banana bread? banana muffins?"] - # The dimensionality of the output embeddings. - dimensionality = 3072 - # The task type for embedding. Check the available tasks in the model's documentation. - task = "RETRIEVAL_DOCUMENT" - - model = TextEmbeddingModel.from_pretrained("gemini-embedding-001") - kwargs = dict(output_dimensionality=dimensionality) if dimensionality else {} - - embeddings = [] - # gemini-embedding-001 takes one input at a time - for text in texts: - text_input = TextEmbeddingInput(text, task) - embedding = model.get_embeddings([text_input], **kwargs) - print(embedding) - # Example response: - # [[0.006135190837085247, -0.01462465338408947, 0.004978656303137541, ...]] - embeddings.append(embedding[0].values) - - return embeddings - - -# [END generativeaionvertexai_embedding] - - -if __name__ == "__main__": - embed_text() diff --git a/generative_ai/embeddings/model_tuning_example.py b/generative_ai/embeddings/model_tuning_example.py deleted file mode 100644 index e2945dbd256..00000000000 --- a/generative_ai/embeddings/model_tuning_example.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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. - -# [START generativeaionvertexai_embedding_model_tuning] -import re - -from google.cloud.aiplatform import initializer as aiplatform_init -from vertexai.language_models import TextEmbeddingModel - - -def tune_embedding_model( - api_endpoint: str, - base_model_name: str = "text-embedding-005", - corpus_path: str = "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/corpus.jsonl", - queries_path: str = "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/queries.jsonl", - train_label_path: str = "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/train.tsv", - test_label_path: str = "gs://cloud-samples-data/ai-platform/embedding/goog-10k-2024/r11/test.tsv", -): # noqa: ANN201 - """Tune an embedding model using the specified parameters. - Args: - api_endpoint (str): The API endpoint for the Vertex AI service. - base_model_name (str): The name of the base model to use for tuning. - corpus_path (str): GCS URI of the JSONL file containing the corpus data. - queries_path (str): GCS URI of the JSONL file containing the queries data. - train_label_path (str): GCS URI of the TSV file containing the training labels. - test_label_path (str): GCS URI of the TSV file containing the test labels. - """ - match = re.search(r"^(\w+-\w+)", api_endpoint) - location = match.group(1) if match else "us-central1" - base_model = TextEmbeddingModel.from_pretrained(base_model_name) - tuning_job = base_model.tune_model( - task_type="DEFAULT", - corpus_data=corpus_path, - queries_data=queries_path, - training_data=train_label_path, - test_data=test_label_path, - batch_size=128, # The batch size to use for training. - train_steps=1000, # The number of training steps. - tuned_model_location=location, - output_dimensionality=768, # The dimensionality of the output embeddings. - learning_rate_multiplier=1.0, # The multiplier for the learning rate. - ) - return tuning_job - - -# [END generativeaionvertexai_embedding_model_tuning] -if __name__ == "__main__": - tune_embedding_model(aiplatform_init.global_config.api_endpoint) diff --git a/generative_ai/embeddings/multimodal_image_example.py b/generative_ai/embeddings/multimodal_image_example.py deleted file mode 100644 index 84549f16baf..00000000000 --- a/generative_ai/embeddings/multimodal_image_example.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from vertexai.vision_models import MultiModalEmbeddingResponse - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def get_image_text_embeddings() -> MultiModalEmbeddingResponse: - """Example of how to generate multimodal embeddings from image and text. - - Read more @ https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings#text-image-embedding - """ - # [START generativeaionvertexai_multimodal_embedding_image] - import vertexai - from vertexai.vision_models import Image, MultiModalEmbeddingModel - - # TODO(developer): Update & uncomment line below - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = MultiModalEmbeddingModel.from_pretrained("multimodalembedding@001") - image = Image.load_from_file( - "gs://cloud-samples-data/vertex-ai/llm/prompts/landmark1.png" - ) - - embeddings = model.get_embeddings( - image=image, - contextual_text="Colosseum", - dimension=1408, - ) - print(f"Image Embedding: {embeddings.image_embedding}") - print(f"Text Embedding: {embeddings.text_embedding}") - # Example response: - # Image Embedding: [-0.0123147098, 0.0727171078, ...] - # Text Embedding: [0.00230263756, 0.0278981831, ...] - - # [END generativeaionvertexai_multimodal_embedding_image] - - return embeddings - - -if __name__ == "__main__": - get_image_text_embeddings() diff --git a/generative_ai/embeddings/multimodal_video_example.py b/generative_ai/embeddings/multimodal_video_example.py deleted file mode 100644 index df9dcd31c1f..00000000000 --- a/generative_ai/embeddings/multimodal_video_example.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from vertexai.vision_models import MultiModalEmbeddingResponse - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def get_video_embeddings() -> MultiModalEmbeddingResponse: - """Example of how to use the multimodal embedding model to get embeddings only for video content. - - Read more at https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings#vid-embedding - """ - # [START generativeaionvertexai_multimodal_embedding_video] - import vertexai - - from vertexai.vision_models import MultiModalEmbeddingModel, Video - from vertexai.vision_models import VideoSegmentConfig - - # TODO(developer): Update & uncomment line below - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = MultiModalEmbeddingModel.from_pretrained("multimodalembedding@001") - - embeddings = model.get_embeddings( - video=Video.load_from_file( - "gs://cloud-samples-data/vertex-ai-vision/highway_vehicles.mp4" - ), - video_segment_config=VideoSegmentConfig(end_offset_sec=1), - ) - - # Video Embeddings are segmented based on the video_segment_config. - print("Video Embeddings:") - for video_embedding in embeddings.video_embeddings: - print( - f"Video Segment: {video_embedding.start_offset_sec} - {video_embedding.end_offset_sec}" - ) - print(f"Embedding: {video_embedding.embedding}") - - # Example response: - # Video Embeddings: - # Video Segment: 0.0 - 1.0 - # Embedding: [-0.0206376351, 0.0123456789, ...] - - # [END generativeaionvertexai_multimodal_embedding_video] - - return embeddings - - -if __name__ == "__main__": - get_video_embeddings() diff --git a/generative_ai/embeddings/noxfile_config.py b/generative_ai/embeddings/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/embeddings/noxfile_config.py +++ b/generative_ai/embeddings/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/embeddings/requirements-test.txt b/generative_ai/embeddings/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/embeddings/requirements-test.txt +++ b/generative_ai/embeddings/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/embeddings/requirements.txt b/generative_ai/embeddings/requirements.txt index 13c79e4e255..be325f58d2b 100644 --- a/generative_ai/embeddings/requirements.txt +++ b/generative_ai/embeddings/requirements.txt @@ -1,10 +1,9 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.84.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.29.0 anthropic[vertex]==0.28.0 numpy<3 diff --git a/generative_ai/embeddings/test_embeddings_examples.py b/generative_ai/embeddings/test_embeddings_examples.py index b430b978e2c..46eeddec881 100644 --- a/generative_ai/embeddings/test_embeddings_examples.py +++ b/generative_ai/embeddings/test_embeddings_examples.py @@ -14,28 +14,23 @@ import os import backoff - -from google.api_core.exceptions import FailedPrecondition, ResourceExhausted - -import google.auth - -from google.cloud import aiplatform -from google.cloud.aiplatform import initializer as aiplatform_init - - import batch_example import code_retrieval_example import document_retrieval_example import generate_embeddings_with_lower_dimension +from google.api_core.exceptions import FailedPrecondition, ResourceExhausted +import google.auth +from google.cloud import aiplatform +from google.cloud.aiplatform import initializer as aiplatform_init import model_tuning_example import multimodal_example -import multimodal_image_example -import multimodal_video_example @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=10) def test_embed_text_batch() -> None: - batch_prediction_job = batch_example.embed_text_batch("gs://python-docs-samples-tests/") + batch_prediction_job = batch_example.embed_text_batch( + "gs://python-docs-samples-tests/" + ) assert batch_prediction_job @@ -48,21 +43,6 @@ def test_multimodal_embedding_image_video_text() -> None: assert embeddings.text_embedding is not None -@backoff.on_exception(backoff.expo, ResourceExhausted, max_time=10) -def test_multimodal_embedding_video() -> None: - embeddings = multimodal_video_example.get_video_embeddings() - assert embeddings is not None - assert embeddings.video_embeddings is not None - - -@backoff.on_exception(backoff.expo, ResourceExhausted, max_time=10) -def test_multimodal_embedding_image() -> None: - embeddings = multimodal_image_example.get_image_text_embeddings() - assert embeddings is not None - assert embeddings.image_embedding is not None - assert embeddings.text_embedding is not None - - @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=10) def test_generate_embeddings_with_lower_dimension() -> None: embeddings = ( diff --git a/generative_ai/evaluation/noxfile_config.py b/generative_ai/evaluation/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/evaluation/noxfile_config.py +++ b/generative_ai/evaluation/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/evaluation/pairwise_summarization_quality.py b/generative_ai/evaluation/pairwise_summarization_quality.py deleted file mode 100644 index 88c89871904..00000000000 --- a/generative_ai/evaluation/pairwise_summarization_quality.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from vertexai.preview.evaluation import EvalResult - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def evaluate_output() -> EvalResult: - # [START generativeaionvertexai_evaluation_pairwise_summarization_quality] - import pandas as pd - - import vertexai - from vertexai.generative_models import GenerativeModel - from vertexai.evaluation import ( - EvalTask, - PairwiseMetric, - MetricPromptTemplateExamples, - ) - - # TODO(developer): Update & uncomment line below - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - prompt = """ - Summarize the text such that a five-year-old can understand. - - # Text - - As part of a comprehensive initiative to tackle urban congestion and foster - sustainable urban living, a major city has revealed ambitious plans for an - extensive overhaul of its public transportation system. The project aims not - only to improve the efficiency and reliability of public transit but also to - reduce the city\'s carbon footprint and promote eco-friendly commuting options. - City officials anticipate that this strategic investment will enhance - accessibility for residents and visitors alike, ushering in a new era of - efficient, environmentally conscious urban transportation. - """ - - eval_dataset = pd.DataFrame({"prompt": [prompt]}) - - # Baseline model for pairwise comparison - baseline_model = GenerativeModel("gemini-2.0-flash-lite-001") - - # Candidate model for pairwise comparison - candidate_model = GenerativeModel( - "gemini-2.0-flash-001", generation_config={"temperature": 0.4} - ) - - prompt_template = MetricPromptTemplateExamples.get_prompt_template( - "pairwise_summarization_quality" - ) - - summarization_quality_metric = PairwiseMetric( - metric="pairwise_summarization_quality", - metric_prompt_template=prompt_template, - baseline_model=baseline_model, - ) - - eval_task = EvalTask( - dataset=eval_dataset, - metrics=[summarization_quality_metric], - experiment="pairwise-experiment", - ) - result = eval_task.evaluate(model=candidate_model) - - baseline_model_response = result.metrics_table["baseline_model_response"].iloc[0] - candidate_model_response = result.metrics_table["response"].iloc[0] - winner_model = result.metrics_table[ - "pairwise_summarization_quality/pairwise_choice" - ].iloc[0] - explanation = result.metrics_table[ - "pairwise_summarization_quality/explanation" - ].iloc[0] - - print(f"Baseline's story:\n{baseline_model_response}") - print(f"Candidate's story:\n{candidate_model_response}") - print(f"Winner: {winner_model}") - print(f"Explanation: {explanation}") - # Example response: - # Baseline's story: - # A big city wants to make it easier for people to get around without using cars! They're going to make buses and trains ... - # - # Candidate's story: - # A big city wants to make it easier for people to get around without using cars! ... This will help keep the air clean ... - # - # Winner: CANDIDATE - # Explanation: Both responses adhere to the prompt's constraints, are grounded in the provided text, and ... However, Response B ... - # [END generativeaionvertexai_evaluation_pairwise_summarization_quality] - return result - - -if __name__ == "__main__": - evaluate_output() diff --git a/generative_ai/evaluation/requirements-test.txt b/generative_ai/evaluation/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/evaluation/requirements-test.txt +++ b/generative_ai/evaluation/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/evaluation/requirements.txt b/generative_ai/evaluation/requirements.txt index be13d57d368..7ac0ee30a69 100644 --- a/generative_ai/evaluation/requirements.txt +++ b/generative_ai/evaluation/requirements.txt @@ -1,14 +1,13 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.69.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.38.0 anthropic[vertex]==0.28.0 -langchain-core==0.2.33 -langchain-google-vertexai==1.0.10 +langchain-core==1.4.0 +langchain-google-vertexai==3.2.3 numpy<3 openai==1.68.2 immutabledict==4.2.0 diff --git a/generative_ai/evaluation/test_evaluation.py b/generative_ai/evaluation/test_evaluation.py index 8263eb97709..0473c4b304b 100644 --- a/generative_ai/evaluation/test_evaluation.py +++ b/generative_ai/evaluation/test_evaluation.py @@ -13,14 +13,8 @@ # limitations under the License. import get_rouge_score -import pairwise_summarization_quality def test_create_evaluation_task() -> None: response = get_rouge_score.get_rouge_score() assert response - - -def test_pairwise_evaluation_summarization_quality() -> None: - response = pairwise_summarization_quality.evaluate_output() - assert response diff --git a/generative_ai/extensions/noxfile_config.py b/generative_ai/extensions/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/extensions/noxfile_config.py +++ b/generative_ai/extensions/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/extensions/requirements-test.txt b/generative_ai/extensions/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/extensions/requirements-test.txt +++ b/generative_ai/extensions/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/extensions/requirements.txt b/generative_ai/extensions/requirements.txt index be13d57d368..7ac0ee30a69 100644 --- a/generative_ai/extensions/requirements.txt +++ b/generative_ai/extensions/requirements.txt @@ -1,14 +1,13 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.69.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.38.0 anthropic[vertex]==0.28.0 -langchain-core==0.2.33 -langchain-google-vertexai==1.0.10 +langchain-core==1.4.0 +langchain-google-vertexai==3.2.3 numpy<3 openai==1.68.2 immutabledict==4.2.0 diff --git a/generative_ai/function_calling/noxfile_config.py b/generative_ai/function_calling/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/function_calling/noxfile_config.py +++ b/generative_ai/function_calling/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/function_calling/requirements-test.txt b/generative_ai/function_calling/requirements-test.txt index 3b9949d8513..3a0f817634b 100644 --- a/generative_ai/function_calling/requirements-test.txt +++ b/generative_ai/function_calling/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.24.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/function_calling/requirements.txt b/generative_ai/function_calling/requirements.txt index 2ffbfa4cc60..567c5d05c7a 100644 --- a/generative_ai/function_calling/requirements.txt +++ b/generative_ai/function_calling/requirements.txt @@ -1,3 +1,3 @@ google-auth==2.38.0 openai==1.68.2 -google-cloud-aiplatform==1.86.0 \ No newline at end of file +google-cloud-aiplatform==1.157.0 diff --git a/generative_ai/image_generation/edit_image_inpainting_insert_mask_mode_test.py b/generative_ai/image_generation/edit_image_inpainting_insert_mask_mode_test.py index 1185c60c3c5..bdae7e6041c 100644 --- a/generative_ai/image_generation/edit_image_inpainting_insert_mask_mode_test.py +++ b/generative_ai/image_generation/edit_image_inpainting_insert_mask_mode_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_inpainting_insert_mask_mode @@ -28,6 +29,7 @@ _PROMPT = "beach" +@pytest.mark.skip("imagegeneration@006 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_inpainting_insert_mask_mode() -> None: response = ( diff --git a/generative_ai/image_generation/edit_image_inpainting_insert_mask_test.py b/generative_ai/image_generation/edit_image_inpainting_insert_mask_test.py index 5154baa1fca..5fadcfa78d5 100644 --- a/generative_ai/image_generation/edit_image_inpainting_insert_mask_test.py +++ b/generative_ai/image_generation/edit_image_inpainting_insert_mask_test.py @@ -16,6 +16,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_inpainting_insert_mask @@ -27,6 +28,7 @@ _PROMPT = "hat" +@pytest.mark.skip("imagegeneration@006 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_inpainting_insert_mask() -> None: response = edit_image_inpainting_insert_mask.edit_image_inpainting_insert_mask( diff --git a/generative_ai/image_generation/edit_image_inpainting_remove_mask_mode_test.py b/generative_ai/image_generation/edit_image_inpainting_remove_mask_mode_test.py index 54633a87fee..68dea245513 100644 --- a/generative_ai/image_generation/edit_image_inpainting_remove_mask_mode_test.py +++ b/generative_ai/image_generation/edit_image_inpainting_remove_mask_mode_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_inpainting_remove_mask_mode @@ -28,6 +29,7 @@ _PROMPT = "sports car" +@pytest.mark.skip("imagegeneration@006 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_inpainting_remove_mask_mode() -> None: response = ( diff --git a/generative_ai/image_generation/edit_image_inpainting_remove_mask_test.py b/generative_ai/image_generation/edit_image_inpainting_remove_mask_test.py index 43c965c8bf5..b11b1b1605f 100644 --- a/generative_ai/image_generation/edit_image_inpainting_remove_mask_test.py +++ b/generative_ai/image_generation/edit_image_inpainting_remove_mask_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_inpainting_remove_mask @@ -28,6 +29,7 @@ _PROMPT = "volleyball game" +@pytest.mark.skip("imagegeneration@006 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_inpainting_remove_mask() -> None: response = edit_image_inpainting_remove_mask.edit_image_inpainting_remove_mask( diff --git a/generative_ai/image_generation/edit_image_mask_free_test.py b/generative_ai/image_generation/edit_image_mask_free_test.py index 96b6e717dd2..078578f8bd9 100644 --- a/generative_ai/image_generation/edit_image_mask_free_test.py +++ b/generative_ai/image_generation/edit_image_mask_free_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_mask_free @@ -27,6 +28,7 @@ _PROMPT = "a dog" +@pytest.mark.skip("imagegeneration@002 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_mask_free() -> None: response = edit_image_mask_free.edit_image_mask_free( diff --git a/generative_ai/image_generation/edit_image_mask_test.py b/generative_ai/image_generation/edit_image_mask_test.py index fee71f5ab8a..fa244f6ef73 100644 --- a/generative_ai/image_generation/edit_image_mask_test.py +++ b/generative_ai/image_generation/edit_image_mask_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_mask @@ -28,6 +29,7 @@ _PROMPT = "a big book" +@pytest.mark.skip("imagegeneration@002 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_mask() -> None: response = edit_image_mask.edit_image_mask( diff --git a/generative_ai/image_generation/edit_image_outpainting_mask_test.py b/generative_ai/image_generation/edit_image_outpainting_mask_test.py index e54ba9c5e61..1827d871694 100644 --- a/generative_ai/image_generation/edit_image_outpainting_mask_test.py +++ b/generative_ai/image_generation/edit_image_outpainting_mask_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_outpainting_mask @@ -28,6 +29,7 @@ _PROMPT = "city with skyscrapers" +@pytest.mark.skip("imagegeneration@006 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_outpainting_mask() -> None: response = edit_image_outpainting_mask.edit_image_outpainting_mask( diff --git a/generative_ai/image_generation/edit_image_product_image_test.py b/generative_ai/image_generation/edit_image_product_image_test.py index 487a55435f7..d0256eafc93 100644 --- a/generative_ai/image_generation/edit_image_product_image_test.py +++ b/generative_ai/image_generation/edit_image_product_image_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import edit_image_product_image @@ -27,6 +28,7 @@ _PROMPT = "beach" +@pytest.mark.skip("imagegeneration@006 samples pending deprecation") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_edit_image_product_image() -> None: response = edit_image_product_image.edit_image_product_image( diff --git a/generative_ai/image_generation/get_short_form_image_captions_test.py b/generative_ai/image_generation/get_short_form_image_captions_test.py index ed56049c070..2364d45d306 100644 --- a/generative_ai/image_generation/get_short_form_image_captions_test.py +++ b/generative_ai/image_generation/get_short_form_image_captions_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import get_short_form_image_captions @@ -25,6 +26,7 @@ _INPUT_FILE = os.path.join(_RESOURCES, "cat.png") +@pytest.mark.skip("Sample pending deprecation b/452720552") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_get_short_form_image_captions() -> None: response = get_short_form_image_captions.get_short_form_image_captions( diff --git a/generative_ai/image_generation/get_short_form_image_responses_test.py b/generative_ai/image_generation/get_short_form_image_responses_test.py index 00c7827517a..c901a8734bd 100644 --- a/generative_ai/image_generation/get_short_form_image_responses_test.py +++ b/generative_ai/image_generation/get_short_form_image_responses_test.py @@ -17,6 +17,7 @@ import backoff from google.api_core.exceptions import ResourceExhausted +import pytest import get_short_form_image_responses @@ -26,6 +27,7 @@ _QUESTION = "What breed of cat is this a picture of?" +@pytest.mark.skip("Sample pending deprecation b/452720552") @backoff.on_exception(backoff.expo, ResourceExhausted, max_time=60) def test_get_short_form_image_responses() -> None: response = get_short_form_image_responses.get_short_form_image_responses( diff --git a/generative_ai/image_generation/noxfile_config.py b/generative_ai/image_generation/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/image_generation/noxfile_config.py +++ b/generative_ai/image_generation/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/image_generation/requirements-test.txt b/generative_ai/image_generation/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/image_generation/requirements-test.txt +++ b/generative_ai/image_generation/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/image_generation/requirements.txt b/generative_ai/image_generation/requirements.txt index be13d57d368..7ac0ee30a69 100644 --- a/generative_ai/image_generation/requirements.txt +++ b/generative_ai/image_generation/requirements.txt @@ -1,14 +1,13 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.69.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.38.0 anthropic[vertex]==0.28.0 -langchain-core==0.2.33 -langchain-google-vertexai==1.0.10 +langchain-core==1.4.0 +langchain-google-vertexai==3.2.3 numpy<3 openai==1.68.2 immutabledict==4.2.0 diff --git a/generative_ai/labels/labels_example.py b/generative_ai/labels/labels_example.py deleted file mode 100644 index 23168e7d461..00000000000 --- a/generative_ai/labels/labels_example.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from vertexai.generative_models import GenerationResponse - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def generate_content() -> GenerationResponse: - # [START generativeaionvertexai_gemini_set_labels] - import vertexai - - from vertexai.generative_models import GenerativeModel - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = GenerativeModel("gemini-2.0-flash-001") - - prompt = "What is Generative AI?" - response = model.generate_content( - prompt, - # Example Labels - labels={ - "team": "research", - "component": "frontend", - "environment": "production", - }, - ) - - print(response.text) - # Example response: - # Generative AI is a type of Artificial Intelligence focused on **creating new content** based on existing data. - - # [END generativeaionvertexai_gemini_set_labels] - return response - - -if __name__ == "__main__": - generate_content() diff --git a/generative_ai/labels/requirements-test.txt b/generative_ai/labels/requirements-test.txt deleted file mode 100644 index 2247ce2d832..00000000000 --- a/generative_ai/labels/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -google-api-core==2.23.0 -pytest==8.2.0 diff --git a/generative_ai/labels/requirements.txt b/generative_ai/labels/requirements.txt deleted file mode 100644 index 913473b5ef0..00000000000 --- a/generative_ai/labels/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -google-cloud-aiplatform==1.74.0 diff --git a/generative_ai/model_garden/noxfile_config.py b/generative_ai/model_garden/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/model_garden/noxfile_config.py +++ b/generative_ai/model_garden/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/model_garden/requirements-test.txt b/generative_ai/model_garden/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/model_garden/requirements-test.txt +++ b/generative_ai/model_garden/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/model_garden/requirements.txt b/generative_ai/model_garden/requirements.txt index be13d57d368..7ac0ee30a69 100644 --- a/generative_ai/model_garden/requirements.txt +++ b/generative_ai/model_garden/requirements.txt @@ -1,14 +1,13 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.69.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.38.0 anthropic[vertex]==0.28.0 -langchain-core==0.2.33 -langchain-google-vertexai==1.0.10 +langchain-core==1.4.0 +langchain-google-vertexai==3.2.3 numpy<3 openai==1.68.2 immutabledict==4.2.0 diff --git a/generative_ai/model_tuning/distillation_example.py b/generative_ai/model_tuning/distillation_example.py deleted file mode 100644 index 43ff6374c47..00000000000 --- a/generative_ai/model_tuning/distillation_example.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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. - -# [START generativeaionvertexai_sdk_distillation] -from __future__ import annotations - -import os - -from typing import Optional - -import vertexai -from vertexai.preview.language_models import TextGenerationModel, TuningEvaluationSpec - - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def distill_model( - dataset: str, - source_model: str, - evaluation_dataset: Optional[str] = None, -) -> None: - """Distill a new model using a teacher model and a dataset. - Args: - dataset (str): GCS URI of the JSONL file containing the training data. - E.g., "gs://[BUCKET]/[FILENAME].jsonl". - source_model (str): Name of the teacher model to distill from. - E.g., "text-unicorn@001". - evaluation_dataset (Optional[str]): GCS URI of the JSONL file containing the evaluation data. - """ - # TODO developer - override these parameters as needed: - vertexai.init(project=PROJECT_ID, location="us-central1") - - # Create a tuning evaluation specification with the evaluation dataset - eval_spec = TuningEvaluationSpec(evaluation_data=evaluation_dataset) - - # Load the student model from a pre-trained model - student_model = TextGenerationModel.from_pretrained("text-bison@002") - - # Start the distillation job using the teacher model and dataset - distillation_job = student_model.distill_from( - teacher_model=source_model, - dataset=dataset, - # Optional: - train_steps=300, # Number of training steps to use when tuning the model. - evaluation_spec=eval_spec, - ) - - return distillation_job - - -# [END generativeaionvertexai_sdk_distillation] - -if __name__ == "__main__": - distill_model( - dataset="your-dataset-uri", - source_model="your-source-model", - evaluation_dataset="your-evaluation-dataset-uri", - ) diff --git a/generative_ai/model_tuning/distillation_example_test.py b/generative_ai/model_tuning/distillation_example_test.py deleted file mode 100644 index d7c762eeeab..00000000000 --- a/generative_ai/model_tuning/distillation_example_test.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 uuid - -from google.cloud import aiplatform -from google.cloud import storage -from google.cloud.aiplatform.compat.types import pipeline_state - -import pytest - -from vertexai.preview.language_models import TextGenerationModel - -import distillation_example - - -_BUCKET = os.environ["CLOUD_STORAGE_BUCKET"] - - -def get_model_display_name(tuned_model: TextGenerationModel) -> str: - language_model_tuning_job = tuned_model._job - pipeline_job = language_model_tuning_job._job - return dict(pipeline_job._gca_resource.runtime_config.parameter_values)[ - "model_display_name" - ] - - -def upload_to_gcs(bucket: str, name: str, data: str) -> None: - client = storage.Client() - bucket = client.get_bucket(bucket) - blob = bucket.blob(name) - blob.upload_from_string(data) - - -def download_from_gcs(bucket: str, name: str) -> str: - client = storage.Client() - bucket = client.get_bucket(bucket) - blob = bucket.blob(name) - data = blob.download_as_bytes() - return "\n".join(data.decode().splitlines()[:10]) - - -def delete_from_gcs(bucket: str, name: str) -> None: - client = storage.Client() - bucket = client.get_bucket(bucket) - blob = bucket.blob(name) - blob.delete() - - -@pytest.fixture(scope="function") -def training_data_filename() -> str: - temp_filename = f"{uuid.uuid4()}.jsonl" - data = download_from_gcs( - "cloud-samples-data", "ai-platform/generative_ai/headline_classification.jsonl" - ) - upload_to_gcs(_BUCKET, temp_filename, data) - try: - yield f"gs://{_BUCKET}/{temp_filename}" - finally: - delete_from_gcs(_BUCKET, temp_filename) - - -def teardown_model( - tuned_model: TextGenerationModel, training_data_filename: str -) -> None: - for tuned_model_name in tuned_model.list_tuned_model_names(): - model_registry = aiplatform.models.ModelRegistry(model=tuned_model_name) - if ( - training_data_filename - in model_registry.get_version_info("1").model_display_name - ): - display_name = model_registry.get_version_info("1").model_display_name - for endpoint in aiplatform.Endpoint.list(): - for _ in endpoint.list_models(): - if endpoint.display_name == display_name: - endpoint.undeploy_all() - endpoint.delete() - aiplatform.Model(model_registry.model_resource_name).delete() - - -@pytest.mark.skip("Blocked on b/277959219") -def test_distill_model(training_data_filename: str) -> None: - """Takes approx. 60 minutes.""" - student_model = distillation_example.distill_model( - dataset=training_data_filename, - teacher_model="text-unicorn@001", - evaluation_dataset=training_data_filename, - ) - try: - assert ( - student_model._job.state - == pipeline_state.PipelineState.PIPELINE_STATE_SUCCEEDED - ) - finally: - teardown_model(student_model, training_data_filename) diff --git a/generative_ai/model_tuning/evaluate_model_example.py b/generative_ai/model_tuning/evaluate_model_example.py deleted file mode 100644 index 3ccc352742f..00000000000 --- a/generative_ai/model_tuning/evaluate_model_example.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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. - -# [START generativeaionvertexai_evaluate_model] -import os - -from google.auth import default - -import vertexai -from vertexai.preview.language_models import ( - EvaluationTextClassificationSpec, - TextGenerationModel, -) - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def evaluate_model() -> object: - """Evaluate the performance of a generative AI model.""" - - # Set credentials for the pipeline components used in the evaluation task - credentials, _ = default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) - - vertexai.init(project=PROJECT_ID, location="us-central1", credentials=credentials) - - # Create a reference to a generative AI model - model = TextGenerationModel.from_pretrained("text-bison@002") - - # Define the evaluation specification for a text classification task - task_spec = EvaluationTextClassificationSpec( - ground_truth_data=[ - "gs://cloud-samples-data/ai-platform/generative_ai/llm_classification_bp_input_prompts_with_ground_truth.jsonl" - ], - class_names=["nature", "news", "sports", "health", "startups"], - target_column_name="ground_truth", - ) - - # Evaluate the model - eval_metrics = model.evaluate(task_spec=task_spec) - print(eval_metrics) - # Example response: - # ... - # PipelineJob run completed. - # Resource name: projects/123456789/locations/us-central1/pipelineJobs/evaluation-llm-classification-... - # EvaluationClassificationMetric(label_name=None, auPrc=0.53833705, auRoc=0.8... - - return eval_metrics - - -# [END generativeaionvertexai_evaluate_model] - - -if __name__ == "__main__": - evaluate_model() diff --git a/generative_ai/model_tuning/noxfile_config.py b/generative_ai/model_tuning/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/model_tuning/noxfile_config.py +++ b/generative_ai/model_tuning/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/model_tuning/pretrained_codegen_example.py b/generative_ai/model_tuning/pretrained_codegen_example.py deleted file mode 100644 index def942422d1..00000000000 --- a/generative_ai/model_tuning/pretrained_codegen_example.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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. - -# [START generativeaionvertexai_sdk_tune_code_generation_model] -from __future__ import annotations - -import os - -import vertexai -from vertexai.language_models import CodeGenerationModel - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def tune_code_generation_model() -> CodeGenerationModel: - # Initialize Vertex AI - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = CodeGenerationModel.from_pretrained("code-bison@002") - - # TODO(developer): Update the training data path - tuning_job = model.tune_model( - training_data="gs://cloud-samples-data/ai-platform/generative_ai/headline_classification.jsonl", - tuning_job_location="europe-west4", - tuned_model_location="us-central1", - ) - - print(tuning_job._status) - # Example response: - # ... - # pipeline_job = aiplatform.PipelineJob.get('projects/1234567890/locations/europe-west4/pipelineJobs/tune... - # PipelineState.PIPELINE_STATE_PENDING - return model - - -# [END generativeaionvertexai_sdk_tune_code_generation_model] - -if __name__ == "__main__": - tune_code_generation_model() diff --git a/generative_ai/model_tuning/pretrained_examples_test.py b/generative_ai/model_tuning/pretrained_examples_test.py deleted file mode 100644 index fcfe7512f89..00000000000 --- a/generative_ai/model_tuning/pretrained_examples_test.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 backoff - -from google.api_core.exceptions import ResourceExhausted - -from google.cloud import aiplatform - -import pytest - -from vertexai.language_models import TextGenerationModel - -import pretrained_codegen_example -import pretrained_list_example -import pretrained_textgen_example - - -def teardown_model(tuned_model: TextGenerationModel) -> None: - for tuned_model_name in tuned_model.list_tuned_model_names(): - model_registry = aiplatform.models.ModelRegistry(model=tuned_model_name) - - display_name = model_registry.get_version_info("1").model_display_name - for endpoint in aiplatform.Endpoint.list(): - for _ in endpoint.list_models(): - if endpoint.display_name == display_name: - endpoint.undeploy_all() - endpoint.delete() - aiplatform.Model(model_registry.model_resource_name).delete() - - -@pytest.mark.skip("Blocked on b/277959219") -def test_tuning_code_generation_model() -> None: - """Takes approx. 20 minutes.""" - tuned_model = pretrained_codegen_example.tune_code_generation_model() - try: - assert tuned_model - finally: - teardown_model(tuned_model) - - -@pytest.mark.skip("Blocked on b/277959219") -def test_tuning() -> None: - """Takes approx. 20 minutes.""" - tuned_model = pretrained_textgen_example.tuning() - try: - assert tuned_model - finally: - teardown_model(tuned_model) - - -@backoff.on_exception(backoff.expo, ResourceExhausted, max_time=10) -def test_list_tuned_models() -> None: - tuned_model_names = pretrained_list_example.list_tuned_models() - filtered_models_counter = 0 - for tuned_model_name in tuned_model_names: - model_registry = aiplatform.models.ModelRegistry(model=tuned_model_name) - if ( - "Vertex LLM Test Fixture " - "(list_tuned_models_test.py::test_list_tuned_models)" - ) in model_registry.get_version_info("1").model_display_name: - filtered_models_counter += 1 - assert filtered_models_counter == 0 diff --git a/generative_ai/model_tuning/pretrained_list_example.py b/generative_ai/model_tuning/pretrained_list_example.py deleted file mode 100644 index 15a03b577bb..00000000000 --- a/generative_ai/model_tuning/pretrained_list_example.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def list_tuned_models() -> None: - """List tuned models.""" - # [START generativeaionvertexai_sdk_list_tuned_models] - import vertexai - - from vertexai.language_models import TextGenerationModel - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = TextGenerationModel.from_pretrained("text-bison@002") - tuned_model_names = model.list_tuned_model_names() - print(tuned_model_names) - # Example response: - # ['projects/1234567890/locations/us-central1/models/1234567889012345', - # ...] - - # [END generativeaionvertexai_sdk_list_tuned_models] - - return tuned_model_names - - -if __name__ == "__main__": - list_tuned_models() diff --git a/generative_ai/model_tuning/pretrained_textgen_example.py b/generative_ai/model_tuning/pretrained_textgen_example.py deleted file mode 100644 index ffee9a4b1b7..00000000000 --- a/generative_ai/model_tuning/pretrained_textgen_example.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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. - -# [START generativeaionvertexai_sdk_tuning] -from __future__ import annotations - -import os - -from vertexai.language_models import TextGenerationModel - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def tuning() -> TextGenerationModel: - import vertexai - from vertexai.language_models import TextGenerationModel - - # Initialize Vertex AI - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = TextGenerationModel.from_pretrained("text-bison@002") - - # TODO(developer): Update the training data path - tuning_job = model.tune_model( - training_data="gs://cloud-samples-data/ai-platform/generative_ai/headline_classification.jsonl", - tuning_job_location="europe-west4", - tuned_model_location="us-central1", - ) - - print(tuning_job._status) - # Example response: - # pipeline_job = aiplatform.PipelineJob.get('projects/1234567890/locations/europe-west4/pipelineJobs/tune... - # View Pipeline Job: - # ... - # PipelineState.PIPELINE_STATE_PENDING - - return model - - -# [END generativeaionvertexai_sdk_tuning] - -if __name__ == "__main__": - tuning() diff --git a/generative_ai/model_tuning/requirements-test.txt b/generative_ai/model_tuning/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/model_tuning/requirements-test.txt +++ b/generative_ai/model_tuning/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/model_tuning/requirements.txt b/generative_ai/model_tuning/requirements.txt index be13d57d368..7ac0ee30a69 100644 --- a/generative_ai/model_tuning/requirements.txt +++ b/generative_ai/model_tuning/requirements.txt @@ -1,14 +1,13 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.69.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.38.0 anthropic[vertex]==0.28.0 -langchain-core==0.2.33 -langchain-google-vertexai==1.0.10 +langchain-core==1.4.0 +langchain-google-vertexai==3.2.3 numpy<3 openai==1.68.2 immutabledict==4.2.0 diff --git a/generative_ai/model_tuning/supervised_advanced_example.py b/generative_ai/model_tuning/supervised_advanced_example.py deleted file mode 100644 index 9e0a7ef11cd..00000000000 --- a/generative_ai/model_tuning/supervised_advanced_example.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from vertexai.tuning import sft - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def gemini_tuning_advanced() -> sft.SupervisedTuningJob: - # [START generativeaionvertexai_tuning_advanced] - - import time - - import vertexai - from vertexai.tuning import sft - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - # Initialize Vertex AI with your service account for BYOSA (Bring Your Own Service Account). - # Uncomment the following and replace "your-service-account" - # vertexai.init(service_account="your-service-account") - - # Initialize Vertex AI with your CMEK (Customer-Managed Encryption Key). - # Un-comment the following line and replace "your-kms-key" - # vertexai.init(encryption_spec_key_name="your-kms-key") - - sft_tuning_job = sft.train( - source_model="gemini-2.0-flash-001", - # 1.5 and 2.0 models use the same JSONL format - train_dataset="gs://cloud-samples-data/ai-platform/generative_ai/gemini-1_5/text/sft_train_data.jsonl", - # The following parameters are optional - validation_dataset="gs://cloud-samples-data/ai-platform/generative_ai/gemini-1_5/text/sft_validation_data.jsonl", - tuned_model_display_name="tuned_gemini_2_0_flash", - # Advanced use only below. It is recommended to use auto-selection and leave them unset - # epochs=4, - # adapter_size=4, - # learning_rate_multiplier=1.0, - ) - - # Polling for job completion - while not sft_tuning_job.has_ended: - time.sleep(60) - sft_tuning_job.refresh() - - print(sft_tuning_job.tuned_model_name) - print(sft_tuning_job.tuned_model_endpoint_name) - print(sft_tuning_job.experiment) - # Example response: - # projects/123456789012/locations/us-central1/models/1234567890@1 - # projects/123456789012/locations/us-central1/endpoints/123456789012345 - # - - # [END generativeaionvertexai_tuning_advanced] - return sft_tuning_job - - -if __name__ == "__main__": - gemini_tuning_advanced() diff --git a/generative_ai/model_tuning/supervised_example.py b/generative_ai/model_tuning/supervised_example.py deleted file mode 100644 index f537a51bdbb..00000000000 --- a/generative_ai/model_tuning/supervised_example.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from vertexai.tuning import sft - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def gemini_tuning_basic() -> sft.SupervisedTuningJob: - # [START generativeaionvertexai_tuning_basic] - - import time - - import vertexai - from vertexai.tuning import sft - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - sft_tuning_job = sft.train( - source_model="gemini-2.0-flash-001", - # 1.5 and 2.0 models use the same JSONL format - train_dataset="gs://cloud-samples-data/ai-platform/generative_ai/gemini-1_5/text/sft_train_data.jsonl", - ) - - # Polling for job completion - while not sft_tuning_job.has_ended: - time.sleep(60) - sft_tuning_job.refresh() - - print(sft_tuning_job.tuned_model_name) - print(sft_tuning_job.tuned_model_endpoint_name) - print(sft_tuning_job.experiment) - # Example response: - # projects/123456789012/locations/us-central1/models/1234567890@1 - # projects/123456789012/locations/us-central1/endpoints/123456789012345 - # - - # [END generativeaionvertexai_tuning_basic] - return sft_tuning_job - - -if __name__ == "__main__": - gemini_tuning_basic() diff --git a/generative_ai/model_tuning/supervised_get_example.py b/generative_ai/model_tuning/supervised_get_example.py deleted file mode 100644 index b1908c2bee8..00000000000 --- a/generative_ai/model_tuning/supervised_get_example.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from vertexai.tuning import sft - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") -LOCATION = "us-central1" - - -def get_tuning_job() -> sft.SupervisedTuningJob: - # [START generativeaionvertexai_get_tuning_job] - import vertexai - from vertexai.tuning import sft - - # TODO(developer): Update and un-comment below lines - # PROJECT_ID = "your-project-id" - # LOCATION = "us-central1" - vertexai.init(project=PROJECT_ID, location=LOCATION) - - tuning_job_id = "4982013113894174720" - response = sft.SupervisedTuningJob( - f"projects/{PROJECT_ID}/locations/{LOCATION}/tuningJobs/{tuning_job_id}" - ) - - print(response) - # Example response: - # - # resource name: projects/1234567890/locations/us-central1/tuningJobs/4982013113894174720 - - # [END generativeaionvertexai_get_tuning_job] - return response - - -if __name__ == "__main__": - get_tuning_job() diff --git a/generative_ai/model_tuning/supervised_list_example.py b/generative_ai/model_tuning/supervised_list_example.py deleted file mode 100644 index 9c882fb2822..00000000000 --- a/generative_ai/model_tuning/supervised_list_example.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -from typing import List - -from vertexai.tuning import sft - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def list_tuning_jobs() -> List[sft.SupervisedTuningJob]: - # [START generativeaionvertexai_list_tuning_jobs] - import vertexai - from vertexai.tuning import sft - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - responses = sft.SupervisedTuningJob.list() - - for response in responses: - print(response) - # Example response: - # - # resource name: projects/12345678/locations/us-central1/tuningJobs/123456789012345 - - # [END generativeaionvertexai_list_tuning_jobs] - return responses - - -if __name__ == "__main__": - list_tuning_jobs() diff --git a/generative_ai/model_tuning/tune_code_generation_model.py b/generative_ai/model_tuning/tune_code_generation_model.py deleted file mode 100644 index d6aab9981d5..00000000000 --- a/generative_ai/model_tuning/tune_code_generation_model.py +++ /dev/null @@ -1,53 +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 -# -# 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. - -from __future__ import annotations - -import os - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def tune_code_generation_model() -> None: - # [START generativeaionvertexai_tune_code_generation_model] - import vertexai - from vertexai.language_models import CodeGenerationModel - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - - # Initialize Vertex AI - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = CodeGenerationModel.from_pretrained("code-bison@002") - - # TODO(developer): Update the training data path - tuning_job = model.tune_model( - training_data="gs://cloud-samples-data/ai-platform/generative_ai/headline_classification.jsonl", - tuning_job_location="europe-west4", - tuned_model_location="us-central1", - ) - - print(tuning_job._status) - # Example response: - # ... - # pipeline_job = aiplatform.PipelineJob.get('projects/1234567890/locations/europe-west4/pipelineJobs/tune... - # PipelineState.PIPELINE_STATE_PENDING - - # [END generativeaionvertexai_tune_code_generation_model] - return model - - -if __name__ == "__main__": - tune_code_generation_model() diff --git a/generative_ai/model_tuning/tuning.py b/generative_ai/model_tuning/tuning.py deleted file mode 100644 index 6001e569659..00000000000 --- a/generative_ai/model_tuning/tuning.py +++ /dev/null @@ -1,56 +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 -# -# 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. - -from __future__ import annotations - -import os - -from vertexai.language_models import TextGenerationModel - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def tuning() -> TextGenerationModel: - # [START generativeaionvertexai_tuning] - import vertexai - from vertexai.language_models import TextGenerationModel - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - - # Initialize Vertex AI - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = TextGenerationModel.from_pretrained("text-bison@002") - - # TODO(developer): Update the training data path - tuning_job = model.tune_model( - training_data="gs://cloud-samples-data/ai-platform/generative_ai/headline_classification.jsonl", - tuning_job_location="europe-west4", - tuned_model_location="us-central1", - ) - - print(tuning_job._status) - # Example response: - # pipeline_job = aiplatform.PipelineJob.get('projects/1234567890/locations/europe-west4/pipelineJobs/tune... - # View Pipeline Job: - # ... - # PipelineState.PIPELINE_STATE_PENDING - - # [END generativeaionvertexai_tuning] - return model - - -if __name__ == "__main__": - tuning() diff --git a/generative_ai/prompts/noxfile_config.py b/generative_ai/prompts/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/prompts/noxfile_config.py +++ b/generative_ai/prompts/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/prompts/requirements-test.txt b/generative_ai/prompts/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/prompts/requirements-test.txt +++ b/generative_ai/prompts/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/prompts/requirements.txt b/generative_ai/prompts/requirements.txt index 30b8dfcdd9f..7ac0ee30a69 100644 --- a/generative_ai/prompts/requirements.txt +++ b/generative_ai/prompts/requirements.txt @@ -1,14 +1,13 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.74.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.38.0 anthropic[vertex]==0.28.0 -langchain-core==0.2.33 -langchain-google-vertexai==1.0.10 +langchain-core==1.4.0 +langchain-google-vertexai==3.2.3 numpy<3 openai==1.68.2 immutabledict==4.2.0 diff --git a/generative_ai/provisioned_throughput/provisioned_throughput_with_txt.py b/generative_ai/provisioned_throughput/provisioned_throughput_with_txt.py deleted file mode 100644 index 8da294ab6aa..00000000000 --- a/generative_ai/provisioned_throughput/provisioned_throughput_with_txt.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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. -import os - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def generate_content() -> str: - # [START generativeaionvertexai_provisioned_throughput_with_txt] - import vertexai - from vertexai.generative_models import GenerativeModel - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - vertexai.init( - project=PROJECT_ID, - location="us-central1", - # Options: - # - "dedicated": Use Provisioned Throughput - # - "shared": Use pay-as-you-go - # https://cloud.google.com/vertex-ai/generative-ai/docs/use-provisioned-throughput - request_metadata=[("x-vertex-ai-llm-request-type", "shared")], - ) - - model = GenerativeModel("gemini-2.0-flash-001") - - response = model.generate_content( - "What's a good name for a flower shop that specializes in selling bouquets of dried flowers?" - ) - - print(response.text) - # Example response: - # **Emphasizing the Dried Aspect:** - # * Everlasting Blooms - # * Dried & Delightful - # * The Petal Preserve - # ... - - # [END generativeaionvertexai_provisioned_throughput_with_txt] - return response.text - - -if __name__ == "__main__": - generate_content() diff --git a/generative_ai/provisioned_throughput/requirements-test.txt b/generative_ai/provisioned_throughput/requirements-test.txt deleted file mode 100644 index 92281986e50..00000000000 --- a/generative_ai/provisioned_throughput/requirements-test.txt +++ /dev/null @@ -1,4 +0,0 @@ -backoff==2.2.1 -google-api-core==2.19.0 -pytest==8.2.0 -pytest-asyncio==0.23.6 diff --git a/generative_ai/provisioned_throughput/requirements.txt b/generative_ai/provisioned_throughput/requirements.txt deleted file mode 100644 index 7131687faca..00000000000 --- a/generative_ai/provisioned_throughput/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -google-cloud-aiplatform==1.82.0 -google-auth==2.38.0 diff --git a/generative_ai/rag/create_corpus_example.py b/generative_ai/rag/create_corpus_example.py index 90b1aa60401..f5bec091527 100644 --- a/generative_ai/rag/create_corpus_example.py +++ b/generative_ai/rag/create_corpus_example.py @@ -15,7 +15,7 @@ from typing import Optional -from vertexai.preview.rag import RagCorpus +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -23,33 +23,36 @@ def create_corpus( display_name: Optional[str] = None, description: Optional[str] = None, -) -> RagCorpus: +) -> types.RagCorpus: # [START generativeaionvertexai_rag_create_corpus] - from vertexai import rag - import vertexai + import agentplatform + from agentplatform import types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # display_name = "test_corpus" # description = "Corpus Description" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") - # Configure backend_config - backend_config = rag.RagVectorDbConfig( - rag_embedding_model_config=rag.RagEmbeddingModelConfig( - vertex_prediction_endpoint=rag.VertexPredictionEndpoint( - publisher_model="publishers/google/models/text-embedding-005" + # Configure project-level config + backend_config = types.RagVectorDbConfig( + rag_embedding_model_config=types.RagEmbeddingModelConfig( + vertex_prediction_endpoint=types.RagEmbeddingModelConfigVertexPredictionEndpoint( + endpoint="publishers/google/models/text-embedding-005" ) ) ) - corpus = rag.create_corpus( - display_name=display_name, - description=description, - backend_config=backend_config, + # Create a corpus + corpus = client.rag.create_corpus( + rag_corpus=types.RagCorpus( + display_name=display_name, + description=description, + rag_vector_db_config=backend_config, + ) ) print(corpus) # Example response: diff --git a/generative_ai/rag/create_corpus_feature_store_example.py b/generative_ai/rag/create_corpus_feature_store_example.py index 8674887c1fe..39f61898361 100644 --- a/generative_ai/rag/create_corpus_feature_store_example.py +++ b/generative_ai/rag/create_corpus_feature_store_example.py @@ -15,7 +15,7 @@ from typing import Optional -from vertexai.preview.rag import RagCorpus +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -24,11 +24,11 @@ def create_corpus_feature_store( feature_view_name: str, display_name: Optional[str] = None, description: Optional[str] = None, -) -> RagCorpus: +) -> types.RagCorpus: # [START generativeaionvertexai_rag_create_corpus_feature_store] - from vertexai.preview import rag - import vertexai + import agentplatform + from agentplatform import types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" @@ -36,22 +36,27 @@ def create_corpus_feature_store( # display_name = "test_corpus" # description = "Corpus Description" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") # Configure embedding model (Optional) - embedding_model_config = rag.EmbeddingModelConfig( - publisher_model="publishers/google/models/text-embedding-004" + backend_config = types.RagVectorDbConfig( + rag_embedding_model_config=types.RagEmbeddingModelConfig( + vertex_prediction_endpoint=types.RagEmbeddingModelConfigVertexPredictionEndpoint( + endpoint="publishers/google/models/text-embedding-005" + ), + ), + vertex_feature_store=types.RagVectorDbConfigVertexFeatureStore( + feature_view_resource_name=feature_view_name + ) ) - # Configure Vector DB - vector_db = rag.VertexFeatureStore(resource_name=feature_view_name) - - corpus = rag.create_corpus( - display_name=display_name, - description=description, - embedding_model_config=embedding_model_config, - vector_db=vector_db, + corpus = client.rag.create_corpus( + rag_corpus=types.RagCorpus( + display_name=display_name, + description=description, + rag_vector_db_config=backend_config, + ) ) print(corpus) # Example response: diff --git a/generative_ai/rag/create_corpus_pinecone_example.py b/generative_ai/rag/create_corpus_pinecone_example.py index ebca30385e8..74a2c6081e5 100644 --- a/generative_ai/rag/create_corpus_pinecone_example.py +++ b/generative_ai/rag/create_corpus_pinecone_example.py @@ -15,52 +15,53 @@ from typing import Optional -from vertexai.preview.rag import RagCorpus +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") + def create_corpus_pinecone( pinecone_index_name: str, pinecone_api_key_secret_manager_version: str, display_name: Optional[str] = None, description: Optional[str] = None, -) -> RagCorpus: +) -> types.RagCorpus: # [START generativeaionvertexai_rag_create_corpus_pinecone] - from vertexai import rag - import vertexai + import agentplatform + from agentplatform import types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # pinecone_index_name = "pinecone-index-name" - # pinecone_api_key_secret_manager_version = "projects/{PROJECT_ID}/secrets/{SECRET_NAME}/versions/latest" # display_name = "test_corpus" # description = "Corpus Description" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") # Configure embedding model (Optional) - embedding_model_config = rag.RagEmbeddingModelConfig( - vertex_prediction_endpoint=rag.VertexPredictionEndpoint( - publisher_model="publishers/google/models/text-embedding-005" + embedding_model_config = types.RagEmbeddingModelConfig( + vertex_prediction_endpoint=types.RagEmbeddingModelConfigVertexPredictionEndpoint( + endpoint="publishers/google/models/text-embedding-005" ) ) # Configure Vector DB - vector_db = rag.Pinecone( - index_name=pinecone_index_name, - api_key=pinecone_api_key_secret_manager_version, + vector_db = types.RagVectorDbConfig( + pinecone=types.RagVectorDbConfigPinecone( + index_name=pinecone_index_name, + ), + rag_embedding_model_config=embedding_model_config, ) - corpus = rag.create_corpus( - display_name=display_name, - description=description, - backend_config=rag.RagVectorDbConfig( - rag_embedding_model_config=embedding_model_config, - vector_db=vector_db, - ), + corpus = client.rag.create_corpus( + rag_corpus=types.RagCorpus( + display_name=display_name, + description=description, + rag_vector_db_config=vector_db, + ) ) print(corpus) # Example response: diff --git a/generative_ai/rag/create_corpus_vector_search_example.py b/generative_ai/rag/create_corpus_vector_search_example.py index 5db30008046..8f3905adbd1 100644 --- a/generative_ai/rag/create_corpus_vector_search_example.py +++ b/generative_ai/rag/create_corpus_vector_search_example.py @@ -15,21 +15,20 @@ from typing import Optional -from vertexai.preview.rag import RagCorpus +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - def create_corpus_vector_search( vector_search_index_name: str, vector_search_index_endpoint_name: str, display_name: Optional[str] = None, description: Optional[str] = None, -) -> RagCorpus: +) -> types.RagCorpus: # [START generativeaionvertexai_rag_create_corpus_vector_search] - from vertexai import rag - import vertexai + import agentplatform + from agentplatform import types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" @@ -38,28 +37,31 @@ def create_corpus_vector_search( # display_name = "test_corpus" # description = "Corpus Description" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") # Configure embedding model (Optional) - embedding_model_config = rag.RagEmbeddingModelConfig( - vertex_prediction_endpoint=rag.VertexPredictionEndpoint( - publisher_model="publishers/google/models/text-embedding-005" + embedding_model_config = types.RagEmbeddingModelConfig( + vertex_prediction_endpoint=types.RagEmbeddingModelConfigVertexPredictionEndpoint( + endpoint="publishers/google/models/text-embedding-005" ) ) # Configure Vector DB - vector_db = rag.VertexVectorSearch( - index=vector_search_index_name, index_endpoint=vector_search_index_endpoint_name + vector_db = types.RagVectorDbConfigVertexVectorSearch( + index=vector_search_index_name, + index_endpoint=vector_search_index_endpoint_name ) - corpus = rag.create_corpus( - display_name=display_name, - description=description, - backend_config=rag.RagVectorDbConfig( - rag_embedding_model_config=embedding_model_config, - vector_db=vector_db, - ), + corpus = client.rag.create_corpus( + rag_corpus=types.RagCorpus( + display_name=display_name, + description=description, + rag_vector_db_config=types.RagVectorDbConfig( + rag_embedding_model_config=embedding_model_config, + vertex_vector_search=vector_db, + ), + ) ) print(corpus) # Example response: diff --git a/generative_ai/rag/create_corpus_vertex_ai_search_example.py b/generative_ai/rag/create_corpus_vertex_ai_search_example.py index 6d3fca5ab9c..01231e73a16 100644 --- a/generative_ai/rag/create_corpus_vertex_ai_search_example.py +++ b/generative_ai/rag/create_corpus_vertex_ai_search_example.py @@ -15,7 +15,7 @@ from typing import Optional -from vertexai import rag +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -24,11 +24,11 @@ def create_corpus_vertex_ai_search( vertex_ai_search_engine_name: str, display_name: Optional[str] = None, description: Optional[str] = None, -) -> rag.RagCorpus: +) -> types.RagCorpus: # [START generativeaionvertexai_rag_create_corpus_vertex_ai_search] - from vertexai import rag - import vertexai + import agentplatform + from agentplatform import types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" @@ -36,18 +36,20 @@ def create_corpus_vertex_ai_search( # display_name = "test_corpus" # description = "Corpus Description" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") # Configure Search - vertex_ai_search_config = rag.VertexAiSearchConfig( + vertex_ai_search_config = types.VertexAiSearchConfig( serving_config=f"{vertex_ai_search_engine_name}/servingConfigs/default_search", ) - corpus = rag.create_corpus( - display_name=display_name, - description=description, - vertex_ai_search_config=vertex_ai_search_config, + corpus = client.rag.create_corpus( + rag_corpus=types.RagCorpus( + display_name=display_name, + description=description, + vertex_ai_search_config=vertex_ai_search_config, + ), ) print(corpus) # Example response: diff --git a/generative_ai/rag/create_corpus_weaviate_example.py b/generative_ai/rag/create_corpus_weaviate_example.py index 9823b8332f8..a12452af2e6 100644 --- a/generative_ai/rag/create_corpus_weaviate_example.py +++ b/generative_ai/rag/create_corpus_weaviate_example.py @@ -15,7 +15,7 @@ from typing import Optional -from vertexai.preview.rag import RagCorpus +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -26,11 +26,11 @@ def create_corpus_weaviate( weaviate_api_key_secret_manager_version: str, display_name: Optional[str] = None, description: Optional[str] = None, -) -> RagCorpus: +) -> types.RagCorpus: # [START generativeaionvertexai_rag_create_corpus_weaviate] - from vertexai.preview import rag - import vertexai + import agentplatform + from agentplatform import types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" @@ -40,26 +40,31 @@ def create_corpus_weaviate( # display_name = "test_corpus" # description = "Corpus Description" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") # Configure embedding model (Optional) - embedding_model_config = rag.EmbeddingModelConfig( - publisher_model="publishers/google/models/text-embedding-004" + embedding_model_config = types.RagEmbeddingModelConfig( + vertex_prediction_endpoint=types.RagEmbeddingModelConfigVertexPredictionEndpoint( + endpoint="publishers/google/models/text-embedding-004" + ) ) # Configure Vector DB - vector_db = rag.Weaviate( - weaviate_http_endpoint=weaviate_http_endpoint, + vector_db = types.RagVectorDbConfigWeaviate( + http_endpoint=weaviate_http_endpoint, collection_name=weaviate_collection_name, - api_key=weaviate_api_key_secret_manager_version, ) - corpus = rag.create_corpus( - display_name=display_name, - description=description, - embedding_model_config=embedding_model_config, - vector_db=vector_db, + corpus = client.rag.create_corpus( + rag_corpus=types.RagCorpus( + display_name=display_name, + description=description, + rag_embedding_model_config=embedding_model_config, + rag_vector_db_config=types.RagVectorDbConfig( + weaviate=vector_db + ), + ) ) print(corpus) # Example response: diff --git a/generative_ai/rag/delete_corpus_example.py b/generative_ai/rag/delete_corpus_example.py index 4255110fe14..527126a92a7 100644 --- a/generative_ai/rag/delete_corpus_example.py +++ b/generative_ai/rag/delete_corpus_example.py @@ -20,17 +20,16 @@ def delete_corpus(corpus_name: str) -> None: # [START generativeaionvertexai_rag_delete_corpus] - from vertexai import rag - import vertexai + import agentplatform # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") - rag.delete_corpus(name=corpus_name) + client.rag.delete_corpus(name=corpus_name) print(f"Corpus {corpus_name} deleted.") # Example response: # Successfully deleted the RagCorpus. diff --git a/generative_ai/rag/delete_file_example.py b/generative_ai/rag/delete_file_example.py index e11afc71d96..477f93a97d0 100644 --- a/generative_ai/rag/delete_file_example.py +++ b/generative_ai/rag/delete_file_example.py @@ -20,17 +20,16 @@ def delete_file(file_name: str) -> None: # [START generativeaionvertexai_rag_delete_file] - from vertexai import rag - import vertexai + import agentplatform # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # file_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}/ragFiles/{rag_file_id}" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") - rag.delete_file(name=file_name) + client.rag.delete_file(name=file_name) print(f"File {file_name} deleted.") # Example response: # Successfully deleted the RagFile. diff --git a/generative_ai/rag/generate_content_example.py b/generative_ai/rag/generate_content_example.py index a02b8bfb7f1..6bff7cb1910 100644 --- a/generative_ai/rag/generate_content_example.py +++ b/generative_ai/rag/generate_content_example.py @@ -14,49 +14,51 @@ import os -from vertexai.generative_models import GenerationResponse +from genai import types as genai_types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") def generate_content_with_rag( corpus_name: str, -) -> GenerationResponse: +) -> genai_types.GenerateContentResponse: # [START generativeaionvertexai_rag_generate_content] - from vertexai import rag - from vertexai.generative_models import GenerativeModel, Tool - import vertexai + from google import genai + from google.genai import types as genai_types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") - - rag_retrieval_tool = Tool.from_retrieval( - retrieval=rag.Retrieval( - source=rag.VertexRagStore( + rag_retrieval_tool = genai_types.Tool( + retrieval=genai_types.Retrieval( + vertex_rag_store=genai_types.VertexRagStore( rag_resources=[ - rag.RagResource( - rag_corpus=corpus_name, - # Optional: supply IDs from `rag.list_files()`. - # rag_file_ids=["rag-file-1", "rag-file-2", ...], + genai_types.VertexRagStoreRagResource( + rag_corpus=corpus_name ) ], - rag_retrieval_config=rag.RagRetrievalConfig( + rag_retrieval_config=genai_types.RagRetrievalConfig( top_k=10, - filter=rag.utils.resources.Filter(vector_distance_threshold=0.5), + filter=genai_types.RagRetrievalConfigFilter( + vector_distance_threshold=0.5 + ), ), ), ) ) - rag_model = GenerativeModel( - model_name="gemini-2.0-flash-001", tools=[rag_retrieval_tool] + # Create a GenAI SDK client to make a generate_content request + genai_client = genai.Client(enterprise=True, project=PROJECT_ID, location="us-central1") + + response = genai_client.models.generate_content( + model="gemini-2.5-pro", + contents="Why is the sky blue?", + config=genai_types.GenerateContentConfig( + tools=[rag_retrieval_tool] + ) ) - response = rag_model.generate_content("Why is the sky blue?") print(response.text) # Example response: # The sky appears blue due to a phenomenon called Rayleigh scattering. diff --git a/generative_ai/rag/get_corpus_example.py b/generative_ai/rag/get_corpus_example.py index 849995156d0..2e0a1ae868c 100644 --- a/generative_ai/rag/get_corpus_example.py +++ b/generative_ai/rag/get_corpus_example.py @@ -14,25 +14,24 @@ import os -from google.cloud.aiplatform_v1beta1 import RagCorpus +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") -def get_corpus(corpus_name: str) -> RagCorpus: +def get_corpus(corpus_name: str) -> types.RagCorpus: # [START generativeaionvertexai_rag_get_corpus] - from vertexai import rag - import vertexai + import agentplatform # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") - corpus = rag.get_corpus(name=corpus_name) + corpus = client.rag.get_corpus(name=corpus_name) print(corpus) # Example response: # RagCorpus(name='projects/[PROJECT_ID]/locations/us-central1/ragCorpora/1234567890', diff --git a/generative_ai/rag/get_file_example.py b/generative_ai/rag/get_file_example.py index 90c461ae4d9..af64b386f83 100644 --- a/generative_ai/rag/get_file_example.py +++ b/generative_ai/rag/get_file_example.py @@ -14,25 +14,24 @@ import os -from google.cloud.aiplatform_v1 import RagFile +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") -def get_file(file_name: str) -> RagFile: +def get_file(file_name: str) -> types.RagFile: # [START generativeaionvertexai_rag_get_file] - from vertexai import rag - import vertexai + import agentplatform # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # file_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}/ragFiles/{rag_file_id}" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") - rag_file = rag.get_file(name=file_name) + rag_file = client.rag.get_file(name=file_name) print(rag_file) # Example response: # RagFile(name='projects/1234567890/locations/us-central1/ragCorpora/11111111111/ragFiles/22222222222', diff --git a/generative_ai/rag/import_files_async_example.py b/generative_ai/rag/import_files_async_example.py index 7485b951ff0..8ca344ce9ee 100644 --- a/generative_ai/rag/import_files_async_example.py +++ b/generative_ai/rag/import_files_async_example.py @@ -16,7 +16,7 @@ from typing import List -from google.cloud.aiplatform_v1 import ImportRagFilesResponse +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -24,38 +24,52 @@ async def import_files_async( corpus_name: str, paths: List[str], -) -> ImportRagFilesResponse: +) -> types.ImportRagFilesResponse: # [START generativeaionvertexai_rag_import_files_async] - from vertexai import rag - import vertexai + import agentplatform + from agentplatform import types + + from google.genai import types as genai_types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}" # Supports Google Cloud Storage and Google Drive Links - # paths = ["https://drive.google.com/file/d/123", "gs://my_bucket/my_files_dir"] - - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") - - response = await rag.import_files( - corpus_name=corpus_name, - paths=paths, - transformation_config=rag.TransformationConfig( - rag.ChunkingConfig(chunk_size=512, chunk_overlap=100) - ), - max_embedding_requests_per_min=900, # Optional + # paths = ["https://drive.google.com/file/d/123", "gs://my_bucket/my_files_dir/*"] + + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") + + response = await client.aio.rag.import_files( + name=corpus_name, + import_config=types.ImportRagFilesConfig( + gcs_source=genai_types.GcsSource(uris=[paths[1]]), + google_drive_source=types.GoogleDriveSource( + resource_ids=[ + types.GoogleDriveSourceResourceId( + resource_id=paths[0], + resource_type=types.ResourceType.RESOURCE_TYPE_FILE + ) + ] + ), # optional + rag_file_transformation_config=types.RagFileTransformationConfig( + rag_file_chunking_config=types.RagFileChunkingConfig( + chunk_size=512, + chunk_overlap=100, + ) + ), # optional + max_embedding_requests_per_min=900, # optional + ) ) - result = await response.result() - print(f"Imported {result.imported_rag_files_count} files.") + print(f"Imported {response.imported_rag_files_count} files.") # Example response: # Imported 2 files. # [END generativeaionvertexai_rag_import_files_async] - return result + return response if __name__ == "__main__": diff --git a/generative_ai/rag/import_files_example.py b/generative_ai/rag/import_files_example.py index c21f68c28d2..d2d92192c8a 100644 --- a/generative_ai/rag/import_files_example.py +++ b/generative_ai/rag/import_files_example.py @@ -15,7 +15,7 @@ import os from typing import List -from google.cloud.aiplatform_v1 import ImportRagFilesResponse +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -23,29 +23,46 @@ def import_files( corpus_name: str, paths: List[str], -) -> ImportRagFilesResponse: +) -> types.ImportRagFilesResponse: # [START generativeaionvertexai_rag_import_files] - from vertexai import rag - import vertexai + import agentplatform + from agentplatform import types + + from google.genai import types as genai_types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}" - # paths = ["https://drive.google.com/file/123", "gs://my_bucket/my_files_dir"] # Supports Google Cloud Storage and Google Drive Links - - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") - - response = rag.import_files( - corpus_name=corpus_name, - paths=paths, - transformation_config=rag.TransformationConfig( - rag.ChunkingConfig(chunk_size=512, chunk_overlap=100) - ), - import_result_sink="gs://sample-existing-folder/sample_import_result_unique.ndjson", # Optional, this has to be an existing storage bucket folder, and file name has to be unique (non-existent). - max_embedding_requests_per_min=900, # Optional + + # Supports Google Cloud Storage and Google Drive Links + # paths = ["https://drive.google.com/file/d/123", "gs://my_bucket/my_files_dir/*"] + + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") + + response = client.rag.import_files( + name=corpus_name, + import_config=types.ImportRagFilesConfig( + gcs_source=genai_types.GcsSource(uris=[paths[1]]), + google_drive_source=types.GoogleDriveSource( + resource_ids=[ + types.GoogleDriveSourceResourceId( + resource_id=paths[0], + resource_type=types.ResourceType.RESOURCE_TYPE_FILE + ) + ] + ), # optional + rag_file_transformation_config=types.RagFileTransformationConfig( + rag_file_chunking_config=types.RagFileChunkingConfig( + chunk_size=512, + chunk_overlap=100, + ) + ), # optional + max_embedding_requests_per_min=900, # optional + ) ) + print(f"Imported {response.imported_rag_files_count} files.") # Example response: # Imported 2 files. diff --git a/generative_ai/rag/list_corpora_example.py b/generative_ai/rag/list_corpora_example.py index 138a47f4330..1e9ba774384 100644 --- a/generative_ai/rag/list_corpora_example.py +++ b/generative_ai/rag/list_corpora_example.py @@ -14,26 +14,23 @@ import os -from google.cloud.aiplatform_v1beta1.services.vertex_rag_data_service.pagers import ( - ListRagCorporaPager, -) +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") -def list_corpora() -> ListRagCorporaPager: +def list_corpora() -> types.ListRagCorporaResponse: # [START generativeaionvertexai_rag_list_corpora] - from vertexai import rag - import vertexai + import agentplatform # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") - corpora = rag.list_corpora() + corpora = client.rag.list_corpora() print(corpora) # Example response: # ListRagCorporaPager ListRagFilesPager: +def list_files(corpus_name: str) -> types.ListRagFilesResponse: # [START generativeaionvertexai_rag_list_files] - from vertexai import rag - import vertexai + import agentplatform # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-central1") - files = rag.list_files(corpus_name=corpus_name) - for file in files: + files_response = client.rag.list_files(name=corpus_name) + for file in files_response.rag_files: print(file.display_name) print(file.name) # Example response: @@ -45,7 +42,7 @@ def list_files(corpus_name: str) -> ListRagFilesPager: # projects/1234567890/locations/us-central1/ragCorpora/111111111111/ragFiles/333333333333 # [END generativeaionvertexai_rag_list_files] - return files + return files_response if __name__ == "__main__": diff --git a/generative_ai/rag/noxfile_config.py b/generative_ai/rag/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/rag/noxfile_config.py +++ b/generative_ai/rag/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/rag/quickstart_example.py b/generative_ai/rag/quickstart_example.py index 1a4f2144826..b57ae049e9e 100644 --- a/generative_ai/rag/quickstart_example.py +++ b/generative_ai/rag/quickstart_example.py @@ -14,88 +14,101 @@ import os -from typing import List, Tuple +from typing import Tuple -from vertexai import rag -from vertexai.generative_models import GenerationResponse +from agentplatform import types +from google.genai import types as genai_types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +MODEL_ID = os.getenv("MODEL_ID") def quickstart( display_name: str, - paths: List[str], -) -> Tuple[rag.RagCorpus, GenerationResponse]: + gcs_path: str, +) -> Tuple[types.RagCorpus, genai_types.GenerateContentResponse]: # [START generativeaionvertexai_rag_quickstart] - from vertexai import rag - from vertexai.generative_models import GenerativeModel, Tool - import vertexai + import agentplatform + + from agentplatform import types + from google import genai + from google.genai import types as genai_types + # Create a RAG Corpus, Import Files, and Generate a response # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" + # MODEL_ID = "gemini-3.5-flash" # display_name = "test_corpus" - # paths = ["https://drive.google.com/file/d/123", "gs://my_bucket/my_files_dir"] # Supports Google Cloud Storage and Google Drive Links + # gcs_path = "gs://my_bucket/my_files_dir/*" + # google_drive_path ="https://drive.google.com/file/d/123" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-east4") - # Create RagCorpus # Configure embedding model, for example "text-embedding-005". - embedding_model_config = rag.RagEmbeddingModelConfig( - vertex_prediction_endpoint=rag.VertexPredictionEndpoint( - publisher_model="publishers/google/models/text-embedding-005" - ) + embedding_model_config = types.RagEmbeddingModelConfig( + vertex_prediction_endpoint=types.RagEmbeddingModelConfigVertexPredictionEndpoint( + endpoint="publishers/google/models/text-embedding-005" + ), ) - rag_corpus = rag.create_corpus( - display_name=display_name, - backend_config=rag.RagVectorDbConfig( - rag_embedding_model_config=embedding_model_config - ), + # Create RagCorpus + rag_corpus = client.rag.create_corpus( + rag_corpus=types.RagCorpus( + display_name=display_name, + rag_vector_db_config=types.RagVectorDbConfig( + rag_embedding_model_config=embedding_model_config + ) + ) ) # Import Files to the RagCorpus - rag.import_files( - rag_corpus.name, - paths, - # Optional - transformation_config=rag.TransformationConfig( - chunking_config=rag.ChunkingConfig( - chunk_size=512, - chunk_overlap=100, - ), - ), - max_embedding_requests_per_min=1000, # Optional + client.rag.import_files( + name=rag_corpus.name, + import_config=types.ImportRagFilesConfig( + gcs_source=genai_types.GcsSource(uris=[gcs_path]), + rag_file_transformation_config=types.RagFileTransformationConfig( + rag_file_chunking_config=types.RagFileChunkingConfig( + chunk_size=512, + chunk_overlap=100, + ) + ), # optional + max_embedding_requests_per_min=1000, # optional + ) ) # Direct context retrieval - rag_retrieval_config = rag.RagRetrievalConfig( + rag_retrieval_config = genai_types.RagRetrievalConfig( top_k=3, # Optional - filter=rag.Filter(vector_distance_threshold=0.5), # Optional + filter=genai_types.RagRetrievalConfigFilter( + vector_distance_threshold=0.5 + ), # Optional ) - response = rag.retrieval_query( - rag_resources=[ - rag.RagResource( - rag_corpus=rag_corpus.name, - # Optional: supply IDs from `rag.list_files()`. - # rag_file_ids=["rag-file-1", "rag-file-2", ...], - ) - ], - text="What is RAG and why it is helpful?", - rag_retrieval_config=rag_retrieval_config, + response = client.rag.retrieve_contexts( + vertex_rag_store=genai_types.VertexRagStore( + rag_resources=[ + genai_types.VertexRagStoreRagResource( + rag_corpus=rag_corpus.name, + ) + ], + ), + query=types.RagQuery( + text="What is RAG and why it is helpful?", + rag_retrieval_config=rag_retrieval_config, + ) ) print(response) # Enhance generation # Create a RAG retrieval tool - rag_retrieval_tool = Tool.from_retrieval( - retrieval=rag.Retrieval( - source=rag.VertexRagStore( + rag_retrieval_tool = genai_types.Tool( + retrieval=genai_types.Retrieval( + vertex_rag_store=genai_types.VertexRagStore( rag_resources=[ - rag.RagResource( - rag_corpus=rag_corpus.name, # Currently only 1 corpus is allowed. + genai_types.VertexRagStoreRagResource( + rag_corpus=rag_corpus.name, # Optional: supply IDs from `rag.list_files()`. # rag_file_ids=["rag-file-1", "rag-file-2", ...], ) @@ -105,13 +118,19 @@ def quickstart( ) ) - # Create a Gemini model instance - rag_model = GenerativeModel( - model_name="gemini-2.0-flash-001", tools=[rag_retrieval_tool] - ) + # Call generate_content with the tool using the GenAI SDK + + # Create a GenAI SDK client + genai_client = genai.Client(enterprise=True, project=PROJECT_ID, location="us-east4") + - # Generate response - response = rag_model.generate_content("What is RAG and why it is helpful?") + response = genai_client.models.generate_content( + model=MODEL_ID, + contents="What is RAG and why it is helpful?", + config=genai_types.GenerateContentConfig( + tools=[rag_retrieval_tool] + ) + ) print(response.text) # Example response: # RAG stands for Retrieval-Augmented Generation. diff --git a/generative_ai/rag/requirements-test.txt b/generative_ai/rag/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/rag/requirements-test.txt +++ b/generative_ai/rag/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/rag/requirements.txt b/generative_ai/rag/requirements.txt index d4591122ee1..567a82bafee 100644 --- a/generative_ai/rag/requirements.txt +++ b/generative_ai/rag/requirements.txt @@ -1 +1 @@ -google-cloud-aiplatform==1.87.0 +google-cloud-aiplatform==1.157.0 diff --git a/generative_ai/rag/retrieval_query_example.py b/generative_ai/rag/retrieval_query_example.py index 6d949b8268b..c887298550b 100644 --- a/generative_ai/rag/retrieval_query_example.py +++ b/generative_ai/rag/retrieval_query_example.py @@ -14,39 +14,47 @@ import os -from google.cloud.aiplatform_v1beta1 import RetrieveContextsResponse +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") def retrieval_query( corpus_name: str, -) -> RetrieveContextsResponse: +) -> types.RetrieveContextsResponse: # [START generativeaionvertexai_rag_retrieval_query] - from vertexai import rag - import vertexai + import agentplatform + + from agentplatform import types + from google.genai import types as genai_types # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/[PROJECT_ID]/locations/us-central1/ragCorpora/[rag_corpus_id]" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-east4") - response = rag.retrieval_query( - rag_resources=[ - rag.RagResource( - rag_corpus=corpus_name, - # Optional: supply IDs from `rag.list_files()`. - # rag_file_ids=["rag-file-1", "rag-file-2", ...], - ) - ], - text="Hello World!", - rag_retrieval_config=rag.RagRetrievalConfig( - top_k=10, - filter=rag.utils.resources.Filter(vector_distance_threshold=0.5), + response = client.rag.retrieve_contexts( + vertex_rag_store=genai_types.VertexRagStore( + rag_resources=[ + genai_types.VertexRagStoreRagResource( + rag_corpus=corpus_name, + # Optional: supply IDs from `rag.list_files()`. + # rag_file_ids=["rag-file-1", "rag-file-2", ...], + ) + ], ), + query=types.RagQuery( + text="Hello World!", + rag_retrieval_config=genai_types.RagRetrievalConfig( + top_k=10, + filter=genai_types.RagRetrievalConfigFilter( + vector_distance_threshold=0.5 + ), + ), + ) ) print(response) # Example response: diff --git a/generative_ai/rag/test_rag_examples.py b/generative_ai/rag/test_rag_examples.py index 3d562f5463c..c35b3c63bba 100644 --- a/generative_ai/rag/test_rag_examples.py +++ b/generative_ai/rag/test_rag_examples.py @@ -16,9 +16,6 @@ import os from pathlib import Path -import pytest -import vertexai - import create_corpus_example import create_corpus_feature_store_example import create_corpus_pinecone_example @@ -34,6 +31,7 @@ import import_files_example import list_corpora_example import list_files_example +import pytest import quickstart_example import retrieval_query_example import upload_file_example @@ -47,9 +45,6 @@ GCS_FILE = "gs://cloud-samples-data/generative-ai/pdf/earnings_statement.pdf" -vertexai.init(project=PROJECT_ID, location=LOCATION) - - @pytest.fixture(scope="module", name="test_file") def test_file_fixture() -> None: file_path = Path("./hello.txt") @@ -156,14 +151,14 @@ def test_get_corpus(test_corpus: pytest.fixture) -> None: def test_list_corpora(test_corpus: pytest.fixture) -> None: corpora = list_corpora_example.list_corpora() - assert any(c.display_name == test_corpus.display_name for c in corpora) + assert any(c.display_name == test_corpus.display_name for c in corpora.rag_corpora) def test_upload_file(test_corpus: pytest.fixture, test_file: pytest.fixture) -> None: rag_file = upload_file_example.upload_file(test_corpus.name, test_file) assert rag_file files = list_files_example.list_files(test_corpus.name) - imported_file = next(iter(files)) + imported_file = next(iter(files.rag_files)) delete_file_example.delete_file(imported_file.name) @@ -171,7 +166,7 @@ def test_import_files(test_corpus: pytest.fixture) -> None: response = import_files_example.import_files(test_corpus.name, [GCS_FILE]) assert response.imported_rag_files_count > 0 files = list_files_example.list_files(test_corpus.name) - imported_file = next(iter(files)) + imported_file = next(iter(files.rag_files)) delete_file_example.delete_file(imported_file.name) @@ -182,7 +177,7 @@ async def test_import_files_async(test_corpus: pytest.fixture) -> None: ) assert result.imported_rag_files_count > 0 files = list_files_example.list_files(test_corpus.name) - imported_file = next(iter(files)) + imported_file = next(iter(files.rag_files)) delete_file_example.delete_file(imported_file.name) @@ -193,7 +188,7 @@ def test_get_file(uploaded_file: pytest.fixture) -> None: def test_list_files(test_corpus: pytest.fixture, uploaded_file: pytest.fixture) -> None: files = list_files_example.list_files(test_corpus.name) - assert any(f.name == uploaded_file.name for f in files) + assert any(f.name == uploaded_file.name for f in files.rag_files) def test_retrieval_query(test_corpus: pytest.fixture) -> None: @@ -210,8 +205,8 @@ def test_generate_content_with_rag(test_corpus: pytest.fixture) -> None: def test_quickstart() -> None: corpus, response = quickstart_example.quickstart( - "test_corpus_quickstart", [GCS_FILE] + "test_corpus_quickstart", GCS_FILE ) assert response assert response.text - delete_corpus_example.delete_corpus(corpus.name) + delete_corpus_example.delete_corpus(corpus.name) \ No newline at end of file diff --git a/generative_ai/rag/upload_file_example.py b/generative_ai/rag/upload_file_example.py index f56cf23f2dc..d3d964d700a 100644 --- a/generative_ai/rag/upload_file_example.py +++ b/generative_ai/rag/upload_file_example.py @@ -16,7 +16,7 @@ from typing import Optional -from vertexai import rag +from agentplatform import types PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") @@ -26,31 +26,28 @@ def upload_file( path: str, display_name: Optional[str] = None, description: Optional[str] = None, -) -> rag.RagFile: +) -> types.RagFile: # [START generativeaionvertexai_rag_upload_file] - from vertexai import rag - import vertexai + import agentplatform # TODO(developer): Update and un-comment below lines # PROJECT_ID = "your-project-id" # corpus_name = "projects/{PROJECT_ID}/locations/us-central1/ragCorpora/{rag_corpus_id}" # path = "path/to/local/file.txt" # display_name = "file_display_name" - # description = "file description" - # Initialize Vertex AI API once per session - vertexai.init(project=PROJECT_ID, location="us-central1") + # Initialize Agent Platform client once per session + client = agentplatform.Client(project=PROJECT_ID, location="us-east4") - rag_file = rag.upload_file( + rag_file = client.rag.upload_file( corpus_name=corpus_name, path=path, display_name=display_name, - description=description, ) print(rag_file) # RagFile(name='projects/[PROJECT_ID]/locations/us-central1/ragCorpora/1234567890/ragFiles/09876543', - # display_name='file_display_name', description='file description') + # display_name='file_display_name') # [END generativeaionvertexai_rag_upload_file] return rag_file diff --git a/generative_ai/reasoning_engine/noxfile_config.py b/generative_ai/reasoning_engine/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/generative_ai/reasoning_engine/noxfile_config.py +++ b/generative_ai/reasoning_engine/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/generative_ai/reasoning_engine/requirements-test.txt b/generative_ai/reasoning_engine/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/generative_ai/reasoning_engine/requirements-test.txt +++ b/generative_ai/reasoning_engine/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/generative_ai/reasoning_engine/requirements.txt b/generative_ai/reasoning_engine/requirements.txt index be13d57d368..7ac0ee30a69 100644 --- a/generative_ai/reasoning_engine/requirements.txt +++ b/generative_ai/reasoning_engine/requirements.txt @@ -1,14 +1,13 @@ pandas==2.2.3; python_version == '3.7' pandas==2.2.3; python_version == '3.8' pandas==2.2.3; python_version > '3.8' -pillow==10.4.0; python_version < '3.8' -pillow==10.4.0; python_version >= '3.8' -google-cloud-aiplatform[all]==1.69.0 -sentencepiece==0.2.0 +pillow==12.3.0 +google-cloud-aiplatform[full]==1.157.0 +sentencepiece==0.2.1 google-auth==2.38.0 anthropic[vertex]==0.28.0 -langchain-core==0.2.33 -langchain-google-vertexai==1.0.10 +langchain-core==1.4.0 +langchain-google-vertexai==3.2.3 numpy<3 openai==1.68.2 immutabledict==4.2.0 diff --git a/generative_ai/text_generation/text_example01.py b/generative_ai/text_generation/text_example01.py deleted file mode 100644 index 744ec4ee1ed..00000000000 --- a/generative_ai/text_generation/text_example01.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# 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 - -PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") - - -def generate_from_text_input() -> str: - # [START generativeaionvertexai_gemini_generate_from_text_input] - import vertexai - from vertexai.generative_models import GenerativeModel - - # TODO(developer): Update and un-comment below line - # PROJECT_ID = "your-project-id" - vertexai.init(project=PROJECT_ID, location="us-central1") - - model = GenerativeModel("gemini-2.0-flash-001") - - response = model.generate_content( - "What's a good name for a flower shop that specializes in selling bouquets of dried flowers?" - ) - - print(response.text) - # Example response: - # **Emphasizing the Dried Aspect:** - # * Everlasting Blooms - # * Dried & Delightful - # * The Petal Preserve - # ... - - # [END generativeaionvertexai_gemini_generate_from_text_input] - return response.text - - -if __name__ == "__main__": - generate_from_text_input() diff --git a/healthcare/api-client/v1/consent/noxfile_config.py b/healthcare/api-client/v1/consent/noxfile_config.py index ecc5247cce9..52036a424ef 100644 --- a/healthcare/api-client/v1/consent/noxfile_config.py +++ b/healthcare/api-client/v1/consent/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"], + "ignored_versions": ["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/healthcare/api-client/v1/consent/requirements-test.txt b/healthcare/api-client/v1/consent/requirements-test.txt index 0d7187a429e..659ed4bc2c9 100644 --- a/healthcare/api-client/v1/consent/requirements-test.txt +++ b/healthcare/api-client/v1/consent/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" diff --git a/healthcare/api-client/v1/datasets/noxfile_config.py b/healthcare/api-client/v1/datasets/noxfile_config.py index b4d9f9b057c..6cea5520fa8 100644 --- a/healthcare/api-client/v1/datasets/noxfile_config.py +++ b/healthcare/api-client/v1/datasets/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"], + "ignored_versions": ["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/healthcare/api-client/v1/datasets/requirements-test.txt b/healthcare/api-client/v1/datasets/requirements-test.txt index 8b9eaff06c4..cb48e4d986e 100644 --- a/healthcare/api-client/v1/datasets/requirements-test.txt +++ b/healthcare/api-client/v1/datasets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" retrying==1.3.4 diff --git a/healthcare/api-client/v1/dicom/noxfile_config.py b/healthcare/api-client/v1/dicom/noxfile_config.py index b4d9f9b057c..6cea5520fa8 100644 --- a/healthcare/api-client/v1/dicom/noxfile_config.py +++ b/healthcare/api-client/v1/dicom/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"], + "ignored_versions": ["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/healthcare/api-client/v1/dicom/requirements-test.txt b/healthcare/api-client/v1/dicom/requirements-test.txt index 0d7187a429e..659ed4bc2c9 100644 --- a/healthcare/api-client/v1/dicom/requirements-test.txt +++ b/healthcare/api-client/v1/dicom/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" diff --git a/healthcare/api-client/v1/dicom/requirements.txt b/healthcare/api-client/v1/dicom/requirements.txt index 0e536138aa8..5b162858c33 100644 --- a/healthcare/api-client/v1/dicom/requirements.txt +++ b/healthcare/api-client/v1/dicom/requirements.txt @@ -2,4 +2,4 @@ google-api-python-client==2.131.0 google-auth-httplib2==0.2.0 google-auth==2.38.0 google-cloud-pubsub==2.28.0 -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/healthcare/api-client/v1/fhir/noxfile_config.py b/healthcare/api-client/v1/fhir/noxfile_config.py index 1a9099a80eb..c2250035bd5 100644 --- a/healthcare/api-client/v1/fhir/noxfile_config.py +++ b/healthcare/api-client/v1/fhir/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"], + "ignored_versions": ["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/healthcare/api-client/v1/fhir/requirements-test.txt b/healthcare/api-client/v1/fhir/requirements-test.txt index 0d7187a429e..659ed4bc2c9 100644 --- a/healthcare/api-client/v1/fhir/requirements-test.txt +++ b/healthcare/api-client/v1/fhir/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" diff --git a/healthcare/api-client/v1/hl7v2/noxfile_config.py b/healthcare/api-client/v1/hl7v2/noxfile_config.py index b4d9f9b057c..6cea5520fa8 100644 --- a/healthcare/api-client/v1/hl7v2/noxfile_config.py +++ b/healthcare/api-client/v1/hl7v2/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"], + "ignored_versions": ["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/healthcare/api-client/v1/hl7v2/requirements-test.txt b/healthcare/api-client/v1/hl7v2/requirements-test.txt index 0d7187a429e..659ed4bc2c9 100644 --- a/healthcare/api-client/v1/hl7v2/requirements-test.txt +++ b/healthcare/api-client/v1/hl7v2/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" diff --git a/healthcare/api-client/v1beta1/fhir/requirements-test.txt b/healthcare/api-client/v1beta1/fhir/requirements-test.txt index 8ce117fb56e..36921e74453 100644 --- a/healthcare/api-client/v1beta1/fhir/requirements-test.txt +++ b/healthcare/api-client/v1beta1/fhir/requirements-test.txt @@ -1,3 +1,3 @@ backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/healthcare/api-client/v1beta1/fhir/requirements.txt b/healthcare/api-client/v1beta1/fhir/requirements.txt index 70b7172329c..110937d3916 100644 --- a/healthcare/api-client/v1beta1/fhir/requirements.txt +++ b/healthcare/api-client/v1beta1/fhir/requirements.txt @@ -4,4 +4,4 @@ google-auth==2.38.0 google-cloud==0.34.0 google-cloud-storage==2.9.0; python_version < '3.7' google-cloud-storage==2.9.0; python_version > '3.6' -requests==2.31.0 +requests==2.34.2; python_version >= '3.10' diff --git a/iam/api-client/noxfile_config.py b/iam/api-client/noxfile_config.py index 30f1c3971e6..f879875f76a 100644 --- a/iam/api-client/noxfile_config.py +++ b/iam/api-client/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"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Declare optional test sessions you want to opt-in. Currently we # have the following optional test sessions: # 'cloud_run' # Test session for Cloud Run application. diff --git a/iam/api-client/requirements-test.txt b/iam/api-client/requirements-test.txt index 8b9eaff06c4..cb48e4d986e 100644 --- a/iam/api-client/requirements-test.txt +++ b/iam/api-client/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" retrying==1.3.4 diff --git a/iam/cloud-client/snippets/list_keys.py b/iam/cloud-client/snippets/list_keys.py index 781ae742b99..26867f72020 100644 --- a/iam/cloud-client/snippets/list_keys.py +++ b/iam/cloud-client/snippets/list_keys.py @@ -24,7 +24,7 @@ def list_keys(project_id: str, account: str) -> List[iam_admin_v1.ServiceAccountKey]: - """Creates a key for a service account. + """Lists a key for a service account. project_id: ID or number of the Google Cloud project you want to use. account: ID or email which is unique identifier of the service account. diff --git a/iam/cloud-client/snippets/noxfile_config.py b/iam/cloud-client/snippets/noxfile_config.py index 029a70011cc..658f73ff360 100644 --- a/iam/cloud-client/snippets/noxfile_config.py +++ b/iam/cloud-client/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/iam/cloud-client/snippets/requirements-test.txt b/iam/cloud-client/snippets/requirements-test.txt index 6ff70adf77d..975d5ee58c2 100644 --- a/iam/cloud-client/snippets/requirements-test.txt +++ b/iam/cloud-client/snippets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" backoff==2.2.1 diff --git a/iap/app_engine_app/requirements-test.txt b/iap/app_engine_app/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/iap/app_engine_app/requirements-test.txt +++ b/iap/app_engine_app/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/iap/app_engine_app/requirements.txt b/iap/app_engine_app/requirements.txt index f306f93a9ca..76aaddfd791 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; python_version >= '3.9' +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/iap/requirements-test.txt b/iap/requirements-test.txt index 185d62c4204..23df1e03c7e 100644 --- a/iap/requirements-test.txt +++ b/iap/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 diff --git a/iap/requirements.txt b/iap/requirements.txt index 3c2961ba6a2..dff3055efae 100644 --- a/iap/requirements.txt +++ b/iap/requirements.txt @@ -1,9 +1,9 @@ -cryptography==45.0.1 -Flask==3.0.3 +cryptography==48.0.1 +Flask==3.1.3; python_version >= '3.9' google-auth==2.38.0 gunicorn==23.0.0 -requests==2.32.4 +requests==2.34.2 requests-toolbelt==1.0.0 -Werkzeug==3.0.6 +Werkzeug==3.1.8; python_version >= '3.9' google-cloud-iam~=2.17.0 PyJWT~=2.10.1 \ No newline at end of file diff --git a/jobs/v3/api_client/noxfile_config.py b/jobs/v3/api_client/noxfile_config.py index b4d9f9b057c..6cea5520fa8 100644 --- a/jobs/v3/api_client/noxfile_config.py +++ b/jobs/v3/api_client/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"], + "ignored_versions": ["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/jobs/v3/api_client/requirements-test.txt b/jobs/v3/api_client/requirements-test.txt index 2a635ea7b6a..322ae75b68c 100644 --- a/jobs/v3/api_client/requirements-test.txt +++ b/jobs/v3/api_client/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 diff --git a/kms/attestations/noxfile_config.py b/kms/attestations/noxfile_config.py index a68d8a75ecd..932bef266b6 100644 --- a/kms/attestations/noxfile_config.py +++ b/kms/attestations/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"], + "ignored_versions": ["3.8", "3.9", "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/kms/attestations/requirements-test.txt b/kms/attestations/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/kms/attestations/requirements-test.txt +++ b/kms/attestations/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/kms/attestations/requirements.txt b/kms/attestations/requirements.txt index 21fdd0e1147..16ef627e26b 100644 --- a/kms/attestations/requirements.txt +++ b/kms/attestations/requirements.txt @@ -1,4 +1,4 @@ -cryptography==45.0.1 -pem==21.2.0; python_version < '3.8' +cryptography==48.0.1 +pem==23.1.0; python_version < '3.8' pem==23.1.0; python_version > '3.7' -requests==2.31.0 +requests==2.34.2 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/noxfile_config.py b/kms/snippets/noxfile_config.py index 457e86f5413..0973c8621c7 100644 --- a/kms/snippets/noxfile_config.py +++ b/kms/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/kms/snippets/requirements-test.txt b/kms/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/kms/snippets/requirements-test.txt +++ b/kms/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/kms/snippets/requirements.txt b/kms/snippets/requirements.txt index 6e15391cfd6..e0d8475f7e2 100644 --- a/kms/snippets/requirements.txt +++ b/kms/snippets/requirements.txt @@ -1,4 +1,4 @@ -google-cloud-kms==3.2.1 -cryptography==45.0.1 +google-cloud-kms==3.11.0 +cryptography==48.0.1 crcmod==1.7 -jwcrypto==1.5.6 \ No newline at end of file +jwcrypto==1.5.8 \ 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/api-client/requirements-test.txt b/kubernetes_engine/api-client/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/kubernetes_engine/api-client/requirements-test.txt +++ b/kubernetes_engine/api-client/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/kubernetes_engine/django_tutorial/Makefile b/kubernetes_engine/django_tutorial/Makefile index a06da05cc66..eb59513658c 100644 --- a/kubernetes_engine/django_tutorial/Makefile +++ b/kubernetes_engine/django_tutorial/Makefile @@ -10,8 +10,8 @@ create-cluster: .PHONY: create-bucket create-bucket: - gsutil mb gs://$(GCLOUD_PROJECT) - gsutil defacl set public-read gs://$(GCLOUD_PROJECT) + gcloud storage buckets create gs://$(GCLOUD_PROJECT) + gcloud storage buckets update gs://$(GCLOUD_PROJECT) --predefined-default-object-acl=publicRead .PHONY: build build: diff --git a/kubernetes_engine/django_tutorial/requirements-test.txt b/kubernetes_engine/django_tutorial/requirements-test.txt index 15d066af319..b3b2b3ca4cc 100644 --- a/kubernetes_engine/django_tutorial/requirements-test.txt +++ b/kubernetes_engine/django_tutorial/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3 diff --git a/kubernetes_engine/django_tutorial/requirements.txt b/kubernetes_engine/django_tutorial/requirements.txt index 55589ce59e7..3e752a33168 100644 --- a/kubernetes_engine/django_tutorial/requirements.txt +++ b/kubernetes_engine/django_tutorial/requirements.txt @@ -1,10 +1,8 @@ -Django==5.2.5; python_version >= "3.10" -Django==4.2.23; python_version >= "3.8" and python_version < "3.10" +Django==6.0.6; 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 -wheel==0.40.0 +wheel==0.47.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/api/requirements-test.txt b/language/snippets/api/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/language/snippets/api/requirements-test.txt +++ b/language/snippets/api/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/language/snippets/classify_text/noxfile_config.py b/language/snippets/classify_text/noxfile_config.py index 25d1d4e081c..50cd5669209 100644 --- a/language/snippets/classify_text/noxfile_config.py +++ b/language/snippets/classify_text/noxfile_config.py @@ -25,7 +25,7 @@ # > ℹ️ Test only on Python 3.10. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.9", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/language/snippets/classify_text/requirements-test.txt b/language/snippets/classify_text/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/language/snippets/classify_text/requirements-test.txt +++ b/language/snippets/classify_text/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/language/snippets/cloud-client/v1/requirements-test.txt b/language/snippets/cloud-client/v1/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/language/snippets/cloud-client/v1/requirements-test.txt +++ b/language/snippets/cloud-client/v1/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" 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/language/snippets/sentiment/requirements-test.txt b/language/snippets/sentiment/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/language/snippets/sentiment/requirements-test.txt +++ b/language/snippets/sentiment/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/language/v1/requirements-test.txt b/language/v1/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/language/v1/requirements-test.txt +++ b/language/v1/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/language/v2/noxfile_config.py b/language/v2/noxfile_config.py index 38a32880121..b7d8115d773 100644 --- a/language/v2/noxfile_config.py +++ b/language/v2/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/language/v2/requirements-test.txt b/language/v2/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/language/v2/requirements-test.txt +++ b/language/v2/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" 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/import-logs/noxfile_config.py b/logging/import-logs/noxfile_config.py index e0098f56b2f..8ed5845a7a3 100644 --- a/logging/import-logs/noxfile_config.py +++ b/logging/import-logs/noxfile_config.py @@ -17,5 +17,5 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - "ignored_versions": ["2.7", "3.6", "3.7"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], } diff --git a/logging/import-logs/requirements-test.txt b/logging/import-logs/requirements-test.txt index 47c9e1b113d..c00563c2dde 100644 --- a/logging/import-logs/requirements-test.txt +++ b/logging/import-logs/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-logging~=3.11.4 google-cloud-storage~=2.10.0 diff --git a/logging/redaction/Dockerfile b/logging/redaction/Dockerfile index 3d8649357ed..c108cec3dd0 100644 --- a/logging/redaction/Dockerfile +++ b/logging/redaction/Dockerfile @@ -1,5 +1,4 @@ -# From apache/beam_python3.9_sdk:2.43.0 -FROM apache/beam_python3.9_sdk@sha256:0cb6eceed3652d01dd5a555fd9ff4eff5df62161dd99ad53fe591858bdb57741 +FROM apache/beam_python3.9_sdk@sha256:246c4b813c6de8c240b49ed03c426f413f1768321a3c441413031396a08912f9 # Install google-cloud-logging package that is missing in Beam SDK COPY requirements.txt /tmp 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/logging/samples/snippets/quickstart.py b/logging/samples/snippets/quickstart.py new file mode 100644 index 00000000000..7c38ea6fa82 --- /dev/null +++ b/logging/samples/snippets/quickstart.py @@ -0,0 +1,42 @@ +#!/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 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) + + # The data to log + text = "Hello, world!" + + # Writes the log entry + logger.log_text(text) + + print("Logged: {}".format(text)) + # [END logging_quickstart] + + +if __name__ == "__main__": + run_quickstart() diff --git a/logging/samples/snippets/quickstart_test.py b/logging/samples/snippets/quickstart_test.py new file mode 100644 index 00000000000..d8ace2cbcf3 --- /dev/null +++ b/logging/samples/snippets/quickstart_test.py @@ -0,0 +1,22 @@ +# 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 quickstart + + +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..a5a3777fcb2 --- /dev/null +++ b/logging/samples/snippets/requirements-test.txt @@ -0,0 +1,2 @@ +backoff==2.2.1 +pytest==9.0.3 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..71a1e385e52 --- /dev/null +++ b/logging/samples/snippets/snippets.py @@ -0,0 +1,114 @@ +#!/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 +import time + +import google.cloud.logging + +import logging + + +# [START logging_write_log_entry] +def write_entry(): + """Demonstrates how to write log entries to Google Cloud using Python's standard logging library.""" + logging_client = google.cloud.logging.Client() + + # By default, logs route to projects/[PROJECT_ID]/logs/python + # unless overridden by GCP or custom handlers. + logging_client.setup_logging(log_level=logging.INFO) + + # Make a simple text log + logging.info("Hello, world!") + + # Simple text log with severity. + logging.warning("Goodbye, world!") + + # Prepare your structured data as a dictionary. + json_log = { + "name": "King Arthur", + "quest": "Find the Holy Grail", + "favorite_color": "Blue", + } + + logging.info("This is a JSON log.", extra={"json_fields": json_log}) + + # wait for threads to finish working on the background. + time.sleep(5) + + +# [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() + 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..616ce424d48 --- /dev/null +++ b/logging/samples/snippets/snippets_test.py @@ -0,0 +1,81 @@ +# 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 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." +GOOGLE_CLOUD_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT") +DEFAULT_LOGGER = f"projects/{GOOGLE_CLOUD_PROJECT}/logs/python" + + +@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() + + @backoff.on_exception(backoff.expo, AssertionError, max_time=120) + def eventually_consistent_test(): + # retrieve logs + client = logging.Client() + + log_filter = DEFAULT_LOGGER + + entries = client.list_entries( + filter_=log_filter, order_by=logging.DESCENDING, max_results=3 + ) + + retrieved_entries = list(entries) + + assert retrieved_entries[0].payload["message"] == "This is a JSON log." + assert retrieved_entries[1].payload == "Goodbye, world!" + assert retrieved_entries[2].payload == "Hello, world!" + + 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/managedkafka/snippets/connect/clusters/requirements.txt b/managedkafka/snippets/connect/clusters/requirements.txt index 5f372e81c41..7bc747b6a38 100644 --- a/managedkafka/snippets/connect/clusters/requirements.txt +++ b/managedkafka/snippets/connect/clusters/requirements.txt @@ -1,6 +1,6 @@ -protobuf==5.29.4 -pytest==8.2.2 -google-api-core==2.23.0 -google-auth==2.38.0 -google-cloud-managedkafka==0.1.12 -googleapis-common-protos==1.66.0 +protobuf==6.33.6 +pytest==9.0.3 +google-api-core==2.31.0 +google-auth==2.55.1 +google-cloud-managedkafka==0.4.1 +googleapis-common-protos==1.75.0 diff --git a/managedkafka/snippets/requirements.txt b/managedkafka/snippets/requirements.txt index 5f372e81c41..7bc747b6a38 100644 --- a/managedkafka/snippets/requirements.txt +++ b/managedkafka/snippets/requirements.txt @@ -1,6 +1,6 @@ -protobuf==5.29.4 -pytest==8.2.2 -google-api-core==2.23.0 -google-auth==2.38.0 -google-cloud-managedkafka==0.1.12 -googleapis-common-protos==1.66.0 +protobuf==6.33.6 +pytest==9.0.3 +google-api-core==2.31.0 +google-auth==2.55.1 +google-cloud-managedkafka==0.4.1 +googleapis-common-protos==1.75.0 diff --git a/media-translation/snippets/noxfile_config.py b/media-translation/snippets/noxfile_config.py index a68d8a75ecd..932bef266b6 100644 --- a/media-translation/snippets/noxfile_config.py +++ b/media-translation/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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/media-translation/snippets/requirements-test.txt b/media-translation/snippets/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/media-translation/snippets/requirements-test.txt +++ b/media-translation/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/media_cdn/requirements-test.txt b/media_cdn/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/media_cdn/requirements-test.txt +++ b/media_cdn/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/media_cdn/requirements.txt b/media_cdn/requirements.txt index 46e87e778f4..503802346c5 100644 --- a/media_cdn/requirements.txt +++ b/media_cdn/requirements.txt @@ -1,2 +1,2 @@ six==1.16.0 -cryptography==45.0.1 +cryptography==48.0.1 diff --git a/memorystore/memcache/noxfile_config.py b/memorystore/memcache/noxfile_config.py index e70e0f7ff70..0d9b541774e 100644 --- a/memorystore/memcache/noxfile_config.py +++ b/memorystore/memcache/noxfile_config.py @@ -23,7 +23,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. # We only run the cloud run tests in py38 session. - "ignored_versions": ["2.7", "3.6", "3.7"], + "ignored_versions": ["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/memorystore/redis/client_side_metrics/main.py b/memorystore/redis/client_side_metrics/main.py new file mode 100644 index 00000000000..eb47d7002d5 --- /dev/null +++ b/memorystore/redis/client_side_metrics/main.py @@ -0,0 +1,176 @@ +# 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 memorystore_redis_client_side_metrics] +import os +import time + +from opentelemetry import metrics, trace +from opentelemetry.exporter.cloud_monitoring import ( + CloudMonitoringMetricsExporter, +) +from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter +from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +import redis +from redis.exceptions import ConnectionError, TimeoutError + + + + +def init_telemetry(): + """Initializes OpenTelemetry with GCP Exporters and returns the SDK objects.""" + # 1. Initialize Tracing + tracer_provider = TracerProvider() + tracer_provider.add_span_processor( + BatchSpanProcessor(CloudTraceSpanExporter()) + ) + trace.set_tracer_provider(tracer_provider) + tracer = trace.get_tracer("redis.client") + + # 2. Initialize Metrics + metrics_exporter = CloudMonitoringMetricsExporter() + metric_reader = PeriodicExportingMetricReader( + metrics_exporter, export_interval_millis=10000 + ) + meter_provider = MeterProvider(metric_readers=[metric_reader]) + metrics.set_meter_provider(meter_provider) + meter = metrics.get_meter("redis.metrics") + + # Bundle all metric handlers safely into a dictionary + redis_metrics = { + "rtt_hist": meter.create_histogram("redis_client_rtt", unit="ms"), + "client_block_hist": meter.create_histogram( + "redis_client_blocking_latency", unit="ms" + ), + "app_block_hist": meter.create_histogram( + "redis_application_blocking_latency", unit="ms" + ), + "retry_counter": meter.create_counter("redis_retry_count"), + "conn_error_counter": meter.create_counter( + "redis_connectivity_error_count" + ), + } + + redis_metrics["retry_counter"].add(0, {"operation": "startup"}) + redis_metrics["conn_error_counter"].add(0, {"operation": "startup"}) + + # 3. Setup Redis Auto-Instrumentation + RedisInstrumentor().instrument() + + return tracer, redis_metrics, tracer_provider, meter_provider + + +def init_redis_pool(): + """Initializes and returns the Redis ConnectionPool and Client.""" + redis_host = os.environ.get("REDISHOST", "localhost") + redis_port = int(os.environ.get("REDISPORT", 6379)) + + redis_pool = redis.ConnectionPool( + host=redis_host, + port=redis_port, + max_connections=10, + decode_responses=True, + ) + redis_client = redis.Redis(connection_pool=redis_pool) + return redis_pool, redis_client + + +def smart_redis_call( + operation_name, func, redis_pool, metrics, *args, **kwargs +): + """Executes a Redis operation with metrics and retry handling (No Globals!).""" + max_retries = 3 + attempt = 0 + + pool_start = time.time() + try: + conn = redis_pool.get_connection() + redis_pool.release(conn) + except Exception: + pass + + if metrics and metrics.get("client_block_hist"): + metrics["client_block_hist"].record( + (time.time() - pool_start) * 1000, {"operation": operation_name} + ) + + while attempt < max_retries: + try: + req_start = time.time() + response = func(*args, **kwargs) + + if metrics and metrics.get("rtt_hist"): + metrics["rtt_hist"].record( + (time.time() - req_start) * 1000, + {"operation": operation_name}, + ) + + app_start = time.time() + _ = str(response) + + if metrics and metrics.get("app_block_hist"): + metrics["app_block_hist"].record( + (time.time() - app_start) * 1000, + {"operation": operation_name}, + ) + + return response + + except (ConnectionError, TimeoutError) as e: + attempt += 1 + if metrics and metrics.get("conn_error_counter"): + metrics["conn_error_counter"].add( + 1, {"operation": operation_name} + ) + if metrics and metrics.get("retry_counter"): + metrics["retry_counter"].add(1, {"operation": operation_name}) + if attempt >= max_retries: + raise e + time.sleep((2**attempt) * 0.1) + +if __name__ == "__main__": + tracer, redis_metrics, tracer_provider, meter_provider = init_telemetry() + redis_pool, redis_client = init_redis_pool() + + if tracer: + with tracer.start_as_current_span("process_user_span"): + try: + # Simple write and read operations + smart_redis_call( + "set_user", + redis_client.set, + redis_pool, + redis_metrics, + "user:123", + "active", + ) + + result = smart_redis_call( + "get_user", + redis_client.get, + redis_pool, + redis_metrics, + "user:123", + ) + print(f"Retrieved: {result}") + except Exception as e: + print(f"Error: {e}") + + tracer_provider.force_flush() + meter_provider.force_flush() +# [END memorystore_redis_client_side_metrics] diff --git a/memorystore/redis/client_side_metrics/main_test.py b/memorystore/redis/client_side_metrics/main_test.py new file mode 100644 index 00000000000..196d758a54d --- /dev/null +++ b/memorystore/redis/client_side_metrics/main_test.py @@ -0,0 +1,243 @@ +# 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. + +from unittest import mock + +import main +import pytest +from redis.exceptions import ConnectionError + + +def test_smart_redis_call_success(): + """Verifies successful SET and GET execution paths and latency recording.""" + mock_func = mock.MagicMock(return_value="active") + mock_pool = mock.MagicMock() + mock_metrics = { + "client_block_hist": mock.MagicMock(), + "rtt_hist": mock.MagicMock(), + "app_block_hist": mock.MagicMock(), + "retry_counter": mock.MagicMock(), + "conn_error_counter": mock.MagicMock(), + } + + result = main.smart_redis_call( + "set_user", + mock_func, + mock_pool, + mock_metrics, + "user:123", + "active", + ) + + assert result == "active" + mock_func.assert_called_once_with("user:123", "active") + + # Verify all 3 latency histograms were neatly invoked + mock_metrics["client_block_hist"].record.assert_called_once() + mock_metrics["rtt_hist"].record.assert_called_once() + mock_metrics["app_block_hist"].record.assert_called_once() + + # Verify retry counters were untouched + mock_metrics["retry_counter"].add.assert_not_called() + mock_metrics["conn_error_counter"].add.assert_not_called() + + +def test_smart_redis_call_retry_success(): + """Simulates 2 sequential connectivity failures followed by 3rd attempt success.""" + mock_func = mock.MagicMock( + side_effect=[ + ConnectionError("cluster unreachable attempt 1"), + ConnectionError("cluster unreachable attempt 2"), + "success_response", + ] + ) + mock_pool = mock.MagicMock() + mock_metrics = { + "client_block_hist": mock.MagicMock(), + "rtt_hist": mock.MagicMock(), + "app_block_hist": mock.MagicMock(), + "retry_counter": mock.MagicMock(), + "conn_error_counter": mock.MagicMock(), + } + + # Wrap in a sleep patch so the test suite runs in milliseconds + with mock.patch("time.sleep", return_value=None): + result = main.smart_redis_call( + "get_user", + mock_func, + mock_pool, + mock_metrics, + "user:123", + ) + + assert result == "success_response" + assert mock_func.call_count == 3 + + # Verify counters captured exactly 2 retry events + assert mock_metrics["retry_counter"].add.call_count == 2 + assert mock_metrics["conn_error_counter"].add.call_count == 2 + mock_metrics["retry_counter"].add.assert_called_with( + 1, {"operation": "get_user"} + ) + mock_metrics["conn_error_counter"].add.assert_called_with( + 1, {"operation": "get_user"} + ) + + # Success on 3rd attempt means latency was recorded + mock_metrics["app_block_hist"].record.assert_called_once() + + +def test_smart_redis_call_permanent_failure(): + """Verifies that permanent connectivity failures bubble up after 3 attempts.""" + mock_func = mock.MagicMock( + side_effect=ConnectionError("permanent DNS error") + ) + mock_pool = mock.MagicMock() + mock_metrics = { + "client_block_hist": mock.MagicMock(), + "rtt_hist": mock.MagicMock(), + "app_block_hist": mock.MagicMock(), + "retry_counter": mock.MagicMock(), + "conn_error_counter": mock.MagicMock(), + } + + with mock.patch("time.sleep", return_value=None): + with pytest.raises(ConnectionError) as exc_info: + main.smart_redis_call( + "set_user", + mock_func, + mock_pool, + mock_metrics, + "user:123", + "active", + ) + + assert "permanent DNS error" in str(exc_info.value) + assert mock_func.call_count == 3 + + # Verify all 3 attempts were tracked + assert mock_metrics["retry_counter"].add.call_count == 3 + assert mock_metrics["conn_error_counter"].add.call_count == 3 + + # Verify latency was NOT recorded on a permanent failure + mock_metrics["app_block_hist"].record.assert_not_called() + + +def test_init_redis_pool_live(monkeypatch): + """Verifies init_redis_pool correctly parses environment variables without network calls.""" + monkeypatch.setenv("REDISHOST", "10.0.0.1") + monkeypatch.setenv("REDISPORT", "7000") + + redis_pool, redis_client = main.init_redis_pool() + + assert redis_pool is not None + assert redis_client is not None + assert redis_pool.connection_kwargs["host"] == "10.0.0.1" + assert redis_pool.connection_kwargs["port"] == 7000 + + # Test default fallback + monkeypatch.delenv("REDISHOST", raising=False) + monkeypatch.delenv("REDISPORT", raising=False) + + pool_def, r_def = main.init_redis_pool() + assert pool_def.connection_kwargs["host"] == "localhost" + assert pool_def.connection_kwargs["port"] == 6379 + + +def test_smart_redis_call_live_or_skip(): + """Integration Test: Hits a real Redis endpoint if running locally at localhost:6379.""" + import main as live_main + from opentelemetry.sdk.metrics import MeterProvider + from opentelemetry.sdk.trace import TracerProvider + import pytest + import redis + + # Initialize REAL No-op OpenTelemetry SDK providers (invoked directly) + TracerProvider() + mp = MeterProvider() + + try: + # Exercise actual pool creation and connectivity ping + redis_pool, redis_client = live_main.init_redis_pool() + redis_client.ping() + except (redis.exceptions.ConnectionError, redis.exceptions.TimeoutError): + pytest.skip( + "Skipping live Redis test: No running server found at localhost:6379" + ) + + # Bind all 5 real OpenTelemetry No-op metrics for this live execution block + meter = mp.get_meter("test_redis_live") + live_metrics = { + "client_block_hist": meter.create_histogram("client_block"), + "rtt_hist": meter.create_histogram("rtt_hist"), + "app_block_hist": meter.create_histogram("app_block"), + "retry_counter": meter.create_counter("retry_count"), + "conn_error_counter": meter.create_counter("conn_error"), + } + + try: + # Run a real Redis write hitting the ACTUAL endpoint (invoked directly) + live_main.smart_redis_call( + "set_test", + redis_client.set, + redis_pool, + live_metrics, + "test:key", + "live_val", + ) + + result_get = live_main.smart_redis_call( + "get_test", + redis_client.get, + redis_pool, + live_metrics, + "test:key", + ) + + assert result_get == "live_val" + + # Safe DB Cleanup + redis_client.delete("test:key") + + finally: + redis_pool.disconnect() + + +@mock.patch("main.BatchSpanProcessor") +@mock.patch("main.PeriodicExportingMetricReader") +@mock.patch("main.CloudTraceSpanExporter") +@mock.patch("main.CloudMonitoringMetricsExporter") +@mock.patch("main.RedisInstrumentor") +def test_init_telemetry_mocks( + mock_redis_instrumentor, + mock_cloud_metrics, + mock_cloud_trace, + mock_metric_reader, + mock_span_processor, +): + """Verifies the init_telemetry function correctly instruments Redis.""" + tracer, redis_metrics, tp, mp = main.init_telemetry() + + assert tracer is not None + assert redis_metrics is not None + assert tp is not None + assert mp is not None + assert "rtt_hist" in redis_metrics + assert "app_block_hist" in redis_metrics + assert "client_block_hist" in redis_metrics + + mock_redis_instrumentor.return_value.instrument.assert_called_once() + mock_cloud_trace.assert_called_once() + mock_cloud_metrics.assert_called_once() + \ No newline at end of file diff --git a/memorystore/redis/client_side_metrics/requirements-test.txt b/memorystore/redis/client_side_metrics/requirements-test.txt new file mode 100644 index 00000000000..b3b2b3ca4cc --- /dev/null +++ b/memorystore/redis/client_side_metrics/requirements-test.txt @@ -0,0 +1 @@ +pytest==9.0.3 diff --git a/memorystore/redis/client_side_metrics/requirements.txt b/memorystore/redis/client_side_metrics/requirements.txt new file mode 100644 index 00000000000..e8caf96a29a --- /dev/null +++ b/memorystore/redis/client_side_metrics/requirements.txt @@ -0,0 +1,6 @@ +redis==7.0.1 +opentelemetry-api==1.39.1 +opentelemetry-sdk==1.39.1 +opentelemetry-instrumentation-redis==0.60b1 +opentelemetry-exporter-gcp-trace==1.11.0 +opentelemetry-exporter-gcp-monitoring==1.11.0a0 diff --git a/memorystore/redis/cloud_run_deployment/Dockerfile b/memorystore/redis/cloud_run_deployment/Dockerfile index 94548382d70..81e9d8f6404 100644 --- a/memorystore/redis/cloud_run_deployment/Dockerfile +++ b/memorystore/redis/cloud_run_deployment/Dockerfile @@ -14,7 +14,7 @@ # Use the official lightweight Python image. # https://hub.docker.com/_/python -FROM python:3.11-slim +FROM python:3.14-slim # Copy local code to the container image. ENV APP_HOME /app diff --git a/memorystore/redis/gce_deployment/deploy.sh b/memorystore/redis/gce_deployment/deploy.sh index 0fa80846da6..4352fdb8bcf 100755 --- a/memorystore/redis/gce_deployment/deploy.sh +++ b/memorystore/redis/gce_deployment/deploy.sh @@ -37,7 +37,7 @@ fi #Upload the tar to GCS tar -cvf app.tar -C .. requirements.txt main.py # Copy to GCS bucket -gsutil cp app.tar gs://"$GCS_BUCKET_NAME"/gce/ +gcloud storage cp app.tar gs://"$GCS_BUCKET_NAME"/gce/ # Create an instance gcloud compute instances create my-instance \ diff --git a/memorystore/redis/gce_deployment/startup-script.sh b/memorystore/redis/gce_deployment/startup-script.sh index 3e523246114..5a8e0bb0b09 100644 --- a/memorystore/redis/gce_deployment/startup-script.sh +++ b/memorystore/redis/gce_deployment/startup-script.sh @@ -33,7 +33,7 @@ apt-get install -yq \ curl -s "https://storage.googleapis.com/signals-agents/logging/google-fluentd-install.sh" | bash service google-fluentd restart & -gsutil cp gs://"$GCS_BUCKET_NAME"/gce/app.tar /app.tar +gcloud storage cp gs://"$GCS_BUCKET_NAME"/gce/app.tar /app.tar mkdir -p /app tar -x -f /app.tar -C /app cd /app diff --git a/memorystore/redis/noxfile_config.py b/memorystore/redis/noxfile_config.py index bf45881273d..f6a3cf7417f 100644 --- a/memorystore/redis/noxfile_config.py +++ b/memorystore/redis/noxfile_config.py @@ -23,7 +23,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. # We only run the cloud run tests in py38 session. - "ignored_versions": ["2.7", "3.6", "3.7"], + "ignored_versions": ["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/memorystore/redis/requirements-test.txt b/memorystore/redis/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/memorystore/redis/requirements-test.txt +++ b/memorystore/redis/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/memorystore/redis/requirements.txt b/memorystore/redis/requirements.txt index 62c1bce675c..b723bd25ccd 100644 --- a/memorystore/redis/requirements.txt +++ b/memorystore/redis/requirements.txt @@ -11,8 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # [START memorystore_requirements] -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 redis==6.0.0 -Werkzeug==3.0.3 +Werkzeug==3.1.8; python_version >= '3.9' # [END memorystore_requirements] diff --git a/model_armor/snippets/noxfile_config.py b/model_armor/snippets/noxfile_config.py index 29c18b2ba9c..293769315f2 100644 --- a/model_armor/snippets/noxfile_config.py +++ b/model_armor/snippets/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/model_armor/snippets/requirements-test.txt b/model_armor/snippets/requirements-test.txt index 1c987370aa9..c9e154ba440 100644 --- a/model_armor/snippets/requirements-test.txt +++ b/model_armor/snippets/requirements-test.txt @@ -1 +1 @@ -pytest==8.3.4 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/model_armor/snippets/requirements.txt b/model_armor/snippets/requirements.txt index a2d49b77ba7..0b64c19841b 100644 --- a/model_armor/snippets/requirements.txt +++ b/model_armor/snippets/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-modelarmor==0.2.5 +google-cloud-modelarmor==0.2.8 google-cloud-dlp==3.30.0 \ No newline at end of file diff --git a/model_garden/anthropic/anthropic_batchpredict_with_bq.py b/model_garden/anthropic/anthropic_batchpredict_with_bq.py index 1823eb8c266..1e9ecdf0940 100644 --- a/model_garden/anthropic/anthropic_batchpredict_with_bq.py +++ b/model_garden/anthropic/anthropic_batchpredict_with_bq.py @@ -26,7 +26,7 @@ def generate_content(output_uri: str) -> str: # output_uri = f"bq://your-project.your_dataset.your_table" job = client.batches.create( - # Check Anthropic Claude region availability in https://cloud.devsite.corp.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions + # Check Anthropic Claude region availability in https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions # More about Anthropic model: https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-haiku model="publishers/anthropic/models/claude-3-5-haiku", # The source dataset needs to be created specifically in us-east5 diff --git a/model_garden/anthropic/noxfile_config.py b/model_garden/anthropic/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/model_garden/anthropic/noxfile_config.py +++ b/model_garden/anthropic/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/model_garden/anthropic/requirements-test.txt b/model_garden/anthropic/requirements-test.txt index 73541a927f4..de695820000 100644 --- a/model_garden/anthropic/requirements-test.txt +++ b/model_garden/anthropic/requirements-test.txt @@ -1,4 +1,4 @@ google-api-core==2.24.0 google-cloud-bigquery==3.29.0 google-cloud-storage==2.19.0 -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/model_garden/gemma/noxfile_config.py b/model_garden/gemma/noxfile_config.py index 962ba40a926..0973c8621c7 100644 --- a/model_garden/gemma/noxfile_config.py +++ b/model_garden/gemma/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.11", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/model_garden/gemma/requirements-test.txt b/model_garden/gemma/requirements-test.txt index 92281986e50..baa23bf9c3e 100644 --- a/model_garden/gemma/requirements-test.txt +++ b/model_garden/gemma/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1 google-api-core==2.19.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" pytest-asyncio==0.23.6 diff --git a/model_garden/gemma/requirements.txt b/model_garden/gemma/requirements.txt index eba13fe9012..1b2edafeb19 100644 --- a/model_garden/gemma/requirements.txt +++ b/model_garden/gemma/requirements.txt @@ -1 +1 @@ -google-cloud-aiplatform[all]==1.103.0 +google-cloud-aiplatform[full]==1.157.0 diff --git a/monitoring/api/v3/api-client/README.rst b/monitoring/api/v3/api-client/README.rst index b905644a9e8..0b83c81e51f 100644 --- a/monitoring/api/v3/api-client/README.rst +++ b/monitoring/api/v3/api-client/README.rst @@ -1,18 +1,18 @@ .. This file is automatically generated. Do not edit this file directly. -Stackdriver Monitoring Python Samples +Cloud Monitoring Python Samples =============================================================================== .. image:: https://gstatic.com/cloudssh/images/open-btn.png :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=monitoring/api/v3/api-client/README.rst -This directory contains samples for Stackdriver Monitoring. `Stackdriver Monitoring `_ collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch and many others. Stackdriver ingests that data and generates insights via dashboards, charts, and alerts. +This directory contains samples for Cloud Monitoring. `Cloud Monitoring `_ collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch and many others. Cloud Monitoring ingests that data and generates insights via dashboards, charts, and alerts. -.. _Stackdriver Monitoring: https://cloud.google.com/monitoring/docs +.. _Cloud Monitoring: https://cloud.google.com/monitoring/docs Setup ------------------------------------------------------------------------------- @@ -78,7 +78,7 @@ To run this sample: usage: list_resources.py [-h] --project_id PROJECT_ID - Sample command-line program for retrieving Stackdriver Monitoring API V3 + Sample command-line program for retrieving Cloud Monitoring API V3 data. See README.md for instructions on setting up your development environment. @@ -111,7 +111,7 @@ To run this sample: usage: custom_metric.py [-h] --project_id PROJECT_ID - Sample command-line program for writing and reading Stackdriver Monitoring + Sample command-line program for writing and reading Cloud Monitoring API V3 custom metrics. Simple command-line program to demonstrate connecting to the Google diff --git a/monitoring/api/v3/api-client/README.rst.in b/monitoring/api/v3/api-client/README.rst.in index e71bd6de87c..529a45128cb 100644 --- a/monitoring/api/v3/api-client/README.rst.in +++ b/monitoring/api/v3/api-client/README.rst.in @@ -1,15 +1,15 @@ # This file is used to generate README.rst product: - name: Stackdriver Monitoring - short_name: Stackdriver Monitoring + name: Cloud Monitoring + short_name: Cloud Monitoring url: https://cloud.google.com/monitoring/docs description: > - `Stackdriver Monitoring `_ collects metrics, events, and metadata from + `Cloud Monitoring `_ collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch and many - others. Stackdriver ingests that data and generates insights via + others. Cloud Monitoring ingests that data and generates insights via dashboards, charts, and alerts. setup: diff --git a/monitoring/api/v3/api-client/list_resources.py b/monitoring/api/v3/api-client/list_resources.py index e77e610314f..2a4aac9f904 100644 --- a/monitoring/api/v3/api-client/list_resources.py +++ b/monitoring/api/v3/api-client/list_resources.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Sample command-line program for retrieving Stackdriver Monitoring API V3 +"""Sample command-line program for retrieving Cloud Monitoring API V3 data. See README.md for instructions on setting up your development environment. diff --git a/monitoring/api/v3/api-client/requirements-test.txt b/monitoring/api/v3/api-client/requirements-test.txt index 2a635ea7b6a..322ae75b68c 100644 --- a/monitoring/api/v3/api-client/requirements-test.txt +++ b/monitoring/api/v3/api-client/requirements-test.txt @@ -1,4 +1,4 @@ backoff==2.2.1; python_version < "3.7" backoff==2.2.1; python_version >= "3.7" -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" flaky==3.8.1 diff --git a/monitoring/opencensus/requirements-test.txt b/monitoring/opencensus/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/monitoring/opencensus/requirements-test.txt +++ b/monitoring/opencensus/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/monitoring/opencensus/requirements.txt b/monitoring/opencensus/requirements.txt index 77821d121a7..9eeb9b11393 100644 --- a/monitoring/opencensus/requirements.txt +++ b/monitoring/opencensus/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' google-api-core==2.17.1 google-auth==2.38.0 googleapis-common-protos==1.66.0 @@ -7,5 +7,5 @@ opencensus-context==0.1.3 opencensus-ext-prometheus==0.2.1 prometheus-client==0.21.1 prometheus-flask-exporter==0.23.2 -requests==2.31.0 -Werkzeug==3.0.3 +Werkzeug==3.1.8; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' diff --git a/monitoring/prometheus/requirements-test.txt b/monitoring/prometheus/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/monitoring/prometheus/requirements-test.txt +++ b/monitoring/prometheus/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/monitoring/prometheus/requirements.txt b/monitoring/prometheus/requirements.txt index 83b43f830a5..7bd640bb56d 100644 --- a/monitoring/prometheus/requirements.txt +++ b/monitoring/prometheus/requirements.txt @@ -1,8 +1,8 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' google-api-core==2.17.1 google-auth==2.38.0 googleapis-common-protos==1.66.0 prometheus-client==0.21.1 prometheus-flask-exporter==0.23.2 -requests==2.31.0 -Werkzeug==3.0.3 +Werkzeug==3.1.8; python_version >= '3.9' +requests==2.34.2; python_version >= '3.10' diff --git a/monitoring/snippets/v3/alerts-client/README.rst b/monitoring/snippets/v3/alerts-client/README.rst index bb59aad5fee..4f941d6fc14 100644 --- a/monitoring/snippets/v3/alerts-client/README.rst +++ b/monitoring/snippets/v3/alerts-client/README.rst @@ -1,18 +1,18 @@ .. This file is automatically generated. Do not edit this file directly. -Google Stackdriver Alerting API Python Samples +Google Cloud Monitoring Alerting API Python Samples =============================================================================== .. image:: https://gstatic.com/cloudssh/images/open-btn.png :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=monitoring/api/v3/alerts-client/README.rst -This directory contains samples for Google Stackdriver Alerting API. Stackdriver Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch and many others. Stackdriver's Alerting API allows you to create, delete, and make back up copies of your alert policies. +This directory contains samples for Google Cloud Monitoring Alerting API. Cloud Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch and many others. Cloud Monitoring's Alerting API allows you to create, delete, and make back up copies of your alert policies. -.. _Google Stackdriver Alerting API: https://cloud.google.com/monitoring/alerts/ +.. _Google Cloud Monitoring Alerting API: https://cloud.google.com/monitoring/alerts/ To run the sample, you need to enable the API at: https://console.cloud.google.com/apis/library/monitoring.googleapis.com diff --git a/monitoring/snippets/v3/alerts-client/README.rst.in b/monitoring/snippets/v3/alerts-client/README.rst.in index 00b280124ea..c3bd356a750 100644 --- a/monitoring/snippets/v3/alerts-client/README.rst.in +++ b/monitoring/snippets/v3/alerts-client/README.rst.in @@ -1,15 +1,15 @@ # This file is used to generate README.rst product: - name: Google Stackdriver Alerting API - short_name: Stackdriver Alerting API + name: Google Cloud Monitoring Alerting API + short_name: Cloud Monitoring Alerting API url: https://cloud.google.com/monitoring/alerts/ description: > - Stackdriver Monitoring collects metrics, events, and metadata from Google + Cloud Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch - and many others. Stackdriver's Alerting API allows you to create, + and many others. Cloud Monitoring's Alerting API allows you to create, delete, and make back up copies of your alert policies. required_api_url: https://console.cloud.google.com/apis/library/monitoring.googleapis.com diff --git a/monitoring/snippets/v3/alerts-client/noxfile_config.py b/monitoring/snippets/v3/alerts-client/noxfile_config.py index 083b166d18c..f5de96c352a 100644 --- a/monitoring/snippets/v3/alerts-client/noxfile_config.py +++ b/monitoring/snippets/v3/alerts-client/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"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Declare optional test sessions you want to opt-in. Currently we # have the following optional test sessions: # 'cloud_run' # Test session for Cloud Run application. diff --git a/monitoring/snippets/v3/alerts-client/requirements-test.txt b/monitoring/snippets/v3/alerts-client/requirements-test.txt index e312099c33c..568cffaec48 100644 --- a/monitoring/snippets/v3/alerts-client/requirements-test.txt +++ b/monitoring/snippets/v3/alerts-client/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" retrying==1.3.4 flaky==3.8.1 diff --git a/monitoring/snippets/v3/cloud-client/README.rst b/monitoring/snippets/v3/cloud-client/README.rst index 280f9c4e0a7..64e62d36ad9 100644 --- a/monitoring/snippets/v3/cloud-client/README.rst +++ b/monitoring/snippets/v3/cloud-client/README.rst @@ -1,20 +1,20 @@ .. This file is automatically generated. Do not edit this file directly. -Google Stackdriver Monitoring API Python Samples +Google Cloud Monitoring API Python Samples =============================================================================== .. image:: https://gstatic.com/cloudssh/images/open-btn.png :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=monitoring/api/v3/cloud-client/README.rst -This directory contains samples for Google Stackdriver Monitoring API. Stackdriver Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch - and many others. Stackdriver ingests that data and generates insights +This directory contains samples for Google Cloud Monitoring API. Cloud Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch + and many others. Cloud Monitoring ingests that data and generates insights via dashboards, charts, and alerts. -.. _Google Stackdriver Monitoring API: https://cloud.google.com/monitoring/docs/ +.. _Google Cloud Monitoring API: https://cloud.google.com/monitoring/docs/ To run the sample, you need to enable the API at: https://console.cloud.google.com/apis/library/monitoring.googleapis.com diff --git a/monitoring/snippets/v3/cloud-client/README.rst.in b/monitoring/snippets/v3/cloud-client/README.rst.in index 0ab6b2258b7..8ac06335517 100644 --- a/monitoring/snippets/v3/cloud-client/README.rst.in +++ b/monitoring/snippets/v3/cloud-client/README.rst.in @@ -1,15 +1,15 @@ # This file is used to generate README.rst product: - name: Google Stackdriver Monitoring API - short_name: Stackdriver Monitoring API + name: Google Cloud Monitoring API + short_name: Cloud Monitoring API url: https://cloud.google.com/monitoring/docs/ description: > - Stackdriver Monitoring collects metrics, events, and metadata from Google + Cloud Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch - and many others. Stackdriver ingests that data and generates insights + and many others. Cloud Monitoring ingests that data and generates insights via dashboards, charts, and alerts. required_api_url: https://console.cloud.google.com/apis/library/monitoring.googleapis.com diff --git a/monitoring/snippets/v3/cloud-client/noxfile_config.py b/monitoring/snippets/v3/cloud-client/noxfile_config.py index 083b166d18c..f5de96c352a 100644 --- a/monitoring/snippets/v3/cloud-client/noxfile_config.py +++ b/monitoring/snippets/v3/cloud-client/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"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Declare optional test sessions you want to opt-in. Currently we # have the following optional test sessions: # 'cloud_run' # Test session for Cloud Run application. diff --git a/monitoring/snippets/v3/cloud-client/requirements-test.txt b/monitoring/snippets/v3/cloud-client/requirements-test.txt index f3230681cda..79932f83530 100644 --- a/monitoring/snippets/v3/cloud-client/requirements-test.txt +++ b/monitoring/snippets/v3/cloud-client/requirements-test.txt @@ -1,2 +1,2 @@ backoff==2.2.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/monitoring/snippets/v3/uptime-check-client/README.rst b/monitoring/snippets/v3/uptime-check-client/README.rst index 30046bdef9d..b565f9e4884 100644 --- a/monitoring/snippets/v3/uptime-check-client/README.rst +++ b/monitoring/snippets/v3/uptime-check-client/README.rst @@ -1,18 +1,18 @@ .. This file is automatically generated. Do not edit this file directly. -Google Stackdriver Uptime Checks API Python Samples +Google Cloud Monitoring Uptime Checks API Python Samples =============================================================================== .. image:: https://gstatic.com/cloudssh/images/open-btn.png :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=monitoring/api/v3/uptime-check-client/README.rst -This directory contains samples for Google Stackdriver Uptime Checks API. Stackdriver Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch and many others. Stackdriver's Uptime Checks API allows you to create, delete, and list your project's Uptime Checks. +This directory contains samples for Google Cloud Monitoring Uptime Checks API. Cloud Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch and many others. Cloud Monitoring's Uptime Checks API allows you to create, delete, and list your project's Uptime Checks. -.. _Google Stackdriver Uptime Checks API: https://cloud.google.com/monitoring/uptime-checks/management +.. _Google Cloud Monitoring Uptime Checks API: https://cloud.google.com/monitoring/uptime-checks/management Setup ------------------------------------------------------------------------------- diff --git a/monitoring/snippets/v3/uptime-check-client/README.rst.in b/monitoring/snippets/v3/uptime-check-client/README.rst.in index 1174962e48d..84998ed3529 100644 --- a/monitoring/snippets/v3/uptime-check-client/README.rst.in +++ b/monitoring/snippets/v3/uptime-check-client/README.rst.in @@ -1,15 +1,15 @@ # This file is used to generate README.rst product: - name: Google Stackdriver Uptime Checks API - short_name: Stackdriver Uptime Checks API + name: Google Cloud Monitoring Uptime Checks API + short_name: Cloud Uptime Checks API url: https://cloud.google.com/monitoring/uptime-checks/management description: > - Stackdriver Monitoring collects metrics, events, and metadata from Google + Cloud Monitoring collects metrics, events, and metadata from Google Cloud Platform, Amazon Web Services (AWS), hosted uptime probes, application instrumentation, and a variety of common application components including Cassandra, Nginx, Apache Web Server, Elasticsearch - and many others. Stackdriver's Uptime Checks API allows you to create, + and many others. Cloud Monitoring's Uptime Checks API allows you to create, delete, and list your project's Uptime Checks. setup: diff --git a/monitoring/snippets/v3/uptime-check-client/noxfile_config.py b/monitoring/snippets/v3/uptime-check-client/noxfile_config.py index 083b166d18c..f5de96c352a 100644 --- a/monitoring/snippets/v3/uptime-check-client/noxfile_config.py +++ b/monitoring/snippets/v3/uptime-check-client/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"], + "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Declare optional test sessions you want to opt-in. Currently we # have the following optional test sessions: # 'cloud_run' # Test session for Cloud Run application. diff --git a/monitoring/snippets/v3/uptime-check-client/requirements-test.txt b/monitoring/snippets/v3/uptime-check-client/requirements-test.txt index f3230681cda..79932f83530 100644 --- a/monitoring/snippets/v3/uptime-check-client/requirements-test.txt +++ b/monitoring/snippets/v3/uptime-check-client/requirements-test.txt @@ -1,2 +1,2 @@ backoff==2.2.1 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/notebooks/requirements-test.txt b/notebooks/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/notebooks/requirements-test.txt +++ b/notebooks/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/notebooks/samples/requirements-test.txt b/notebooks/samples/requirements-test.txt index 060ed652e0b..c9e154ba440 100644 --- a/notebooks/samples/requirements-test.txt +++ b/notebooks/samples/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 \ No newline at end of file +pytest==9.0.3; python_version >= "3.10" diff --git a/notebooks/tutorials/cloud-ml-engine/Training and prediction with scikit-learn.ipynb b/notebooks/tutorials/cloud-ml-engine/Training and prediction with scikit-learn.ipynb index 4db540c6ae1..d805d0cb1fe 100644 --- a/notebooks/tutorials/cloud-ml-engine/Training and prediction with scikit-learn.ipynb +++ b/notebooks/tutorials/cloud-ml-engine/Training and prediction with scikit-learn.ipynb @@ -76,7 +76,7 @@ "metadata": {}, "outputs": [], "source": [ - "!gsutil mb gs://$BUCKET_NAME/" + "!gcloud storage buckets create gs://$BUCKET_NAME" ] }, { @@ -377,7 +377,7 @@ "metadata": {}, "outputs": [], "source": [ - "!gsutil ls gs://$BUCKET_NAME/" + "!gcloud storage ls gs://$BUCKET_NAME/" ] }, { @@ -539,7 +539,7 @@ "!gcloud ai-platform models delete $MODEL_NAME --quiet\n", "\n", "# Delete the bucket and contents\n", - "!gsutil rm -r gs://$BUCKET_NAME\n", + "!gcloud storage rm --recursive gs://$BUCKET_NAME\n", "\n", "# Delete the local files created by the tutorial\n", "!rm -rf census_training" diff --git a/notebooks/tutorials/storage/Storage command-line tool.ipynb b/notebooks/tutorials/storage/Storage command-line tool.ipynb index 21e62ae8236..eef0054c790 100644 --- a/notebooks/tutorials/storage/Storage command-line tool.ipynb +++ b/notebooks/tutorials/storage/Storage command-line tool.ipynb @@ -26,6 +26,7 @@ }, "outputs": [], "source": [ + "# ERROR: A migration for this command is not implemented. Please refer to the migration guide.\n", "!gsutil help" ] }, @@ -67,7 +68,7 @@ "metadata": {}, "outputs": [], "source": [ - "!gsutil mb gs://{bucket_name}/" + "!gcloud storage buckets create gs://{bucket_name}/" ] }, { @@ -95,7 +96,7 @@ "metadata": {}, "outputs": [], "source": [ - "!gsutil ls -p $project_id" + "!gcloud storage ls --project $project_id" ] }, { @@ -128,7 +129,7 @@ }, "outputs": [], "source": [ - "!gsutil ls -L -b gs://{bucket_name}/" + "!gcloud storage ls --full --buckets gs://{bucket_name}/" ] }, { @@ -163,7 +164,7 @@ "metadata": {}, "outputs": [], "source": [ - "!gsutil cp resources/us-states.txt gs://{bucket_name}/" + "!gcloud storage cp resources/us-states.txt gs://{bucket_name}/" ] }, { @@ -181,7 +182,7 @@ }, "outputs": [], "source": [ - "!gsutil ls -r gs://{bucket_name}/**" + "!gcloud storage ls --recursive gs://{bucket_name}/**" ] }, { @@ -209,7 +210,7 @@ "metadata": {}, "outputs": [], "source": [ - "!gsutil ls -L gs://{bucket_name}/us-states.txt" + "!gcloud storage ls --full gs://{bucket_name}/us-states.txt" ] }, { @@ -245,7 +246,7 @@ }, "outputs": [], "source": [ - "!gsutil cp gs://{bucket_name}/us-states.txt resources/downloaded-us-states.txt" + "!gcloud storage cp gs://{bucket_name}/us-states.txt resources/downloaded-us-states.txt" ] }, { @@ -270,7 +271,7 @@ }, "outputs": [], "source": [ - "!gsutil rm gs://{bucket_name}/us-states.txt" + "!gcloud storage rm gs://{bucket_name}/us-states.txt" ] }, { @@ -288,7 +289,7 @@ "metadata": {}, "outputs": [], "source": [ - "!gsutil rm -r gs://{bucket_name}/" + "!gcloud storage rm --recursive gs://{bucket_name}/" ] }, { 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/noxfile_config.py b/noxfile_config.py index 2a0f115c38f..0973c8621c7 100644 --- a/noxfile_config.py +++ b/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/optimization/snippets/noxfile_config.py b/optimization/snippets/noxfile_config.py index 457e86f5413..0973c8621c7 100644 --- a/optimization/snippets/noxfile_config.py +++ b/optimization/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/optimization/snippets/requirements-test.txt b/optimization/snippets/requirements-test.txt index 24ad1f29818..0161e90f7ad 100644 --- a/optimization/snippets/requirements-test.txt +++ b/optimization/snippets/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/parametermanager/snippets-adk/README.md b/parametermanager/snippets-adk/README.md new file mode 100644 index 00000000000..a2ab8a841eb --- /dev/null +++ b/parametermanager/snippets-adk/README.md @@ -0,0 +1,99 @@ +# ADK Parameter Manager Samples + +This directory contains samples demonstrating how to use the Agent Development Kit (ADK) with Google Cloud Parameter Manager. + +## Folders +* `agent_global`: Sample for accessing parameters from a global Parameter Manager instance. +* `agent_regional`: Sample for accessing parameters from a regional Parameter Manager endpoint. + +## Prerequisites + +1. **Create and activate a virtual environment**: + ```bash + python3 -m venv .venv + source .venv/bin/activate + ``` + +2. **Set up Application Default Credentials**: + ```bash + gcloud auth application-default login + ``` + +3. **Install dependencies**: + You need to install dependencies for the specific sample or test you want to run. + + For global agent samples and tests: + ```bash + pip install -r agent_global/requirements.txt + ``` + + For regional agent samples and tests: + ```bash + pip install -r agent_regional/requirements.txt + ``` + +4. **Set up environment variables**: + * `GOOGLE_CLOUD_PROJECT`: Your Google Cloud Project ID. (Required for both samples and tests). + + The following environment variables are **only required when running the samples manually**. The tests will generate and use their own temporary parameters automatically. + + * `ADK_TEST_PARAMETER_ID`: The ID of the parameter to access. + * `ADK_TEST_PARAMETER_VERSION` (Optional): The version of the parameter (defaults to `latest`). + * `GOOGLE_CLOUD_PROJECT_LOCATION` (Required for regional samples): The region where the parameter is located (e.g., `us-central1`). + +## Running the Samples + +The samples are designed to be run from this directory (`snippets-adk`) using the `adk run` command. + +### Global Parameter Manager Agent + +1. **Create a `.env` file** alongside `agent.py` in the `agent_global` directory: + ```env + GOOGLE_GENAI_USE_VERTEXAI=1 + GOOGLE_CLOUD_PROJECT=your-project-id + GOOGLE_CLOUD_LOCATION=us-central1 + ``` + *Note: Replace `your-project-id` with your GCP project ID.* + +2. **Run the agent**: + ```bash + export GOOGLE_CLOUD_PROJECT="your-project-id" + export ADK_TEST_PARAMETER_ID="your-parameter-id" + adk run agent_global + ``` + +### Regional Parameter Manager Agent + +1. **Create a `.env` file** alongside `agent.py` in the `agent_regional` directory: + ```env + GOOGLE_GENAI_USE_VERTEXAI=1 + GOOGLE_CLOUD_PROJECT=your-project-id + GOOGLE_CLOUD_LOCATION=us-central1 + ``` + *Note: Replace `your-project-id` with your GCP project ID.* + +2. **Run the agent**: + ```bash + export GOOGLE_CLOUD_PROJECT="your-project-id" + export GOOGLE_CLOUD_PROJECT_LOCATION="us-central1" + export ADK_TEST_PARAMETER_ID="your-parameter-id" + adk run agent_regional + ``` + +## Running the Tests + +The tests are located within the specific agent folders and run the samples using `adk run`. + +To run tests for the global agent: +```bash +pip install -r agent_global/requirements.txt +pip install -r agent_global/requirements-test.txt +pytest agent_global/snippets_adk_test.py +``` + +To run tests for the regional agent: +```bash +pip install -r agent_regional/requirements.txt +pip install -r agent_regional/requirements-test.txt +pytest agent_regional/snippets_adk_test.py +``` diff --git a/parametermanager/snippets-adk/agent_global/agent.py b/parametermanager/snippets-adk/agent_global/agent.py new file mode 100644 index 00000000000..3ec915375ae --- /dev/null +++ b/parametermanager/snippets-adk/agent_global/agent.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# 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. +""" +ADK agent for accessing parameters from global Parameter Manager. +""" + +import os + +from google.adk import Agent +from google.adk.integrations.parameter_manager.parameter_client import ParameterManagerClient + +# Fetch parameter from global Parameter Manager +project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") +parameter_id = os.environ.get("ADK_TEST_PARAMETER_ID") +parameter_version = os.environ.get("ADK_TEST_PARAMETER_VERSION", "latest") + +if not project_id or not parameter_id: + raise ValueError("GOOGLE_CLOUD_PROJECT and ADK_TEST_PARAMETER_ID environment variables must be set.") + +resource_name = f"projects/{project_id}/locations/global/parameters/{parameter_id}/versions/{parameter_version}" + +print("Fetching parameter from global Parameter Manager...") +# Initialize Parameter Manager Client +client = ParameterManagerClient() + +# Fetch parameter +try: + parameter_payload = client.get_parameter(resource_name) + print("Successfully fetched parameter.") +except Exception as e: + print(f"Error fetching parameter: {e}") + raise e + +# Initialize Agent +root_agent = Agent( + model='gemini-2.5-flash', + name='root_agent', + description='A helpful assistant for user questions.', + instruction='Answer user questions to the best of your knowledge', +) + +print("Agent initialized successfully.") diff --git a/parametermanager/snippets-adk/agent_global/noxfile_config.py b/parametermanager/snippets-adk/agent_global/noxfile_config.py new file mode 100644 index 00000000000..98a32b2e888 --- /dev/null +++ b/parametermanager/snippets-adk/agent_global/noxfile_config.py @@ -0,0 +1,27 @@ +# 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["3.8", "3.9", "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": True, + # An envvar key for determining the project id to use. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # A dictionary you want to inject into your test. + "envs": {}, +} diff --git a/parametermanager/snippets-adk/agent_global/requirements-test.txt b/parametermanager/snippets-adk/agent_global/requirements-test.txt new file mode 100644 index 00000000000..c9e154ba440 --- /dev/null +++ b/parametermanager/snippets-adk/agent_global/requirements-test.txt @@ -0,0 +1 @@ +pytest==9.0.3; python_version >= "3.10" diff --git a/parametermanager/snippets-adk/agent_global/requirements.txt b/parametermanager/snippets-adk/agent_global/requirements.txt new file mode 100644 index 00000000000..a7a4fdd4908 --- /dev/null +++ b/parametermanager/snippets-adk/agent_global/requirements.txt @@ -0,0 +1 @@ +google-adk[extensions]>=1.30 diff --git a/parametermanager/snippets-adk/agent_global/snippets_adk_test.py b/parametermanager/snippets-adk/agent_global/snippets_adk_test.py new file mode 100644 index 00000000000..57746824045 --- /dev/null +++ b/parametermanager/snippets-adk/agent_global/snippets_adk_test.py @@ -0,0 +1,112 @@ +# 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. + +import os +import subprocess +import time +from typing import Iterator +import uuid + +from google.api_core import exceptions +from google.cloud import parametermanager_v1 +import pytest + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +PARENT_DIR = os.path.dirname(CURRENT_DIR) + + +@pytest.fixture() +def client() -> parametermanager_v1.ParameterManagerClient: + return parametermanager_v1.ParameterManagerClient() + + +@pytest.fixture() +def project_id() -> str: + return os.environ["GOOGLE_CLOUD_PROJECT"] + + +@pytest.fixture() +def parameter_id( + client: parametermanager_v1.ParameterManagerClient, project_id: str +) -> Iterator[str]: + parameter_id = f"python-adk-test-{uuid.uuid4()}" + yield parameter_id + + # Teardown: delete the version then the parameter + version_path = client.parameter_version_path(project_id, "global", parameter_id, "1") + parameter_path = client.parameter_path(project_id, "global", parameter_id) + + print("Deleting parameter version 1...") + try: + time.sleep(1) + client.delete_parameter_version(request={"name": version_path}) + except exceptions.NotFound: + pass + + print(f"Deleting parameter {parameter_id}...") + try: + time.sleep(2) + client.delete_parameter(name=parameter_path) + except exceptions.NotFound: + pass + + +def test_agent_global( + client: parametermanager_v1.ParameterManagerClient, project_id: str, parameter_id: str +) -> None: + # Create the parameter + parent = client.common_location_path(project_id, "global") + print(f"Creating parameter {parameter_id}...") + client.create_parameter( + request={ + "parent": parent, + "parameter_id": parameter_id, + } + ) + + # Add a version + parameter_path = client.parameter_path(project_id, "global", parameter_id) + print(f"Adding version to {parameter_id}...") + client.create_parameter_version( + request={ + "parent": parameter_path, + "parameter_version_id": "1", + "parameter_version": { + "payload": {"data": b"test-payload"} + }, + } + ) + + # Set environment variables required by the agent + os.environ["ADK_TEST_PARAMETER_ID"] = parameter_id + os.environ["ADK_TEST_PARAMETER_VERSION"] = "1" + + print(f"Running adk run agent_global from {PARENT_DIR}...") + # Run the sample using adk run + result = subprocess.run( + ["adk", "run", "agent_global"], + input="exit\n", + capture_output=True, + text=True, + cwd=PARENT_DIR, + ) + + print("STDOUT:") + print(result.stdout) + print("STDERR:") + print(result.stderr) + + assert result.returncode == 0 + assert "Successfully fetched parameter" in result.stdout + assert "Agent initialized successfully" in result.stdout diff --git a/parametermanager/snippets-adk/agent_regional/agent.py b/parametermanager/snippets-adk/agent_regional/agent.py new file mode 100644 index 00000000000..0c633dc8131 --- /dev/null +++ b/parametermanager/snippets-adk/agent_regional/agent.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +# 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. +""" +ADK agent for accessing parameters from regional Parameter Manager. +""" + +import os + +from google.adk import Agent +from google.adk.integrations.parameter_manager.parameter_client import ParameterManagerClient + +# Fetch parameter from regional Parameter Manager +project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") +location = os.environ.get("GOOGLE_CLOUD_PROJECT_LOCATION") +parameter_id = os.environ.get("ADK_TEST_PARAMETER_ID") +parameter_version = os.environ.get("ADK_TEST_PARAMETER_VERSION", "latest") + +if not project_id or not location or not parameter_id: + raise ValueError("GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_PROJECT_LOCATION, and ADK_TEST_PARAMETER_ID environment variables must be set.") + +resource_name = f"projects/{project_id}/locations/{location}/parameters/{parameter_id}/versions/{parameter_version}" + +print(f"Fetching parameter from regional Parameter Manager ({location})...") +# Initialize Parameter Manager Client (Regional) +client = ParameterManagerClient(location=location) + +# Fetch parameter +try: + parameter_payload = client.get_parameter(resource_name) + print("Successfully fetched parameter.") +except Exception as e: + print(f"Error fetching parameter: {e}") + raise e + +# Initialize Agent +root_agent = Agent( + model='gemini-2.5-flash', + name='root_agent', + description='A helpful assistant for user questions.', + instruction='Answer user questions to the best of your knowledge', +) + +print("Agent initialized successfully.") diff --git a/parametermanager/snippets-adk/agent_regional/noxfile_config.py b/parametermanager/snippets-adk/agent_regional/noxfile_config.py new file mode 100644 index 00000000000..98a32b2e888 --- /dev/null +++ b/parametermanager/snippets-adk/agent_regional/noxfile_config.py @@ -0,0 +1,27 @@ +# 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["3.8", "3.9", "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": True, + # An envvar key for determining the project id to use. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # A dictionary you want to inject into your test. + "envs": {}, +} diff --git a/parametermanager/snippets-adk/agent_regional/requirements-test.txt b/parametermanager/snippets-adk/agent_regional/requirements-test.txt new file mode 100644 index 00000000000..c9e154ba440 --- /dev/null +++ b/parametermanager/snippets-adk/agent_regional/requirements-test.txt @@ -0,0 +1 @@ +pytest==9.0.3; python_version >= "3.10" diff --git a/parametermanager/snippets-adk/agent_regional/requirements.txt b/parametermanager/snippets-adk/agent_regional/requirements.txt new file mode 100644 index 00000000000..a7a4fdd4908 --- /dev/null +++ b/parametermanager/snippets-adk/agent_regional/requirements.txt @@ -0,0 +1 @@ +google-adk[extensions]>=1.30 diff --git a/parametermanager/snippets-adk/agent_regional/snippets_adk_test.py b/parametermanager/snippets-adk/agent_regional/snippets_adk_test.py new file mode 100644 index 00000000000..f523a72b073 --- /dev/null +++ b/parametermanager/snippets-adk/agent_regional/snippets_adk_test.py @@ -0,0 +1,124 @@ +# 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. + +import os +import subprocess +import time +from typing import Iterator +import uuid + +from google.api_core import exceptions +from google.cloud import parametermanager_v1 +import pytest + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +PARENT_DIR = os.path.dirname(CURRENT_DIR) + + +@pytest.fixture() +def location() -> str: + return "us-central1" + + +@pytest.fixture() +def client(location: str) -> parametermanager_v1.ParameterManagerClient: + api_endpoint = f"parametermanager.{location}.rep.googleapis.com" + return parametermanager_v1.ParameterManagerClient( + client_options={"api_endpoint": api_endpoint} + ) + + +@pytest.fixture() +def project_id() -> str: + return os.environ["GOOGLE_CLOUD_PROJECT"] + + +@pytest.fixture() +def parameter_id( + client: parametermanager_v1.ParameterManagerClient, project_id: str, location: str +) -> Iterator[str]: + parameter_id = f"python-adk-test-{uuid.uuid4()}" + yield parameter_id + + # Teardown: delete the version then the parameter + version_path = f"projects/{project_id}/locations/{location}/parameters/{parameter_id}/versions/1" + parameter_path = f"projects/{project_id}/locations/{location}/parameters/{parameter_id}" + + print("Deleting parameter version 1...") + try: + time.sleep(1) + client.delete_parameter_version(request={"name": version_path}) + except exceptions.NotFound: + pass + + print(f"Deleting parameter {parameter_id}...") + try: + time.sleep(2) + client.delete_parameter(name=parameter_path) + except exceptions.NotFound: + pass + + +def test_agent_regional( + client: parametermanager_v1.ParameterManagerClient, + project_id: str, + location: str, + parameter_id: str, +) -> None: + # Create the parameter + parent = f"projects/{project_id}/locations/{location}" + print(f"Creating regional parameter {parameter_id} in {location}...") + client.create_parameter( + request={ + "parent": parent, + "parameter_id": parameter_id, + } + ) + + # Add a version + parameter_path = f"projects/{project_id}/locations/{location}/parameters/{parameter_id}" + print(f"Adding version to {parameter_id}...") + client.create_parameter_version( + request={ + "parent": parameter_path, + "parameter_version_id": "1", + "parameter_version": { + "payload": {"data": b"test-payload"} + }, + } + ) + + # Set environment variables required by the agent + os.environ["ADK_TEST_PARAMETER_ID"] = parameter_id + os.environ["ADK_TEST_PARAMETER_VERSION"] = "1" + os.environ["GOOGLE_CLOUD_PROJECT_LOCATION"] = location + + print(f"Running adk run agent_regional from {PARENT_DIR}...") + # Run the sample using adk run + result = subprocess.run( + ["adk", "run", "agent_regional"], + input="exit\n", + capture_output=True, + text=True, + cwd=PARENT_DIR, + ) + + print("STDOUT:") + print(result.stdout) + print("STDERR:") + print(result.stderr) + + assert result.returncode == 0 + assert "Successfully fetched parameter" in result.stdout + assert "Agent initialized successfully" in result.stdout diff --git a/parametermanager/snippets/get_param_version.py b/parametermanager/snippets/get_param_version.py index dace37d53ac..aaf75a4936a 100644 --- a/parametermanager/snippets/get_param_version.py +++ b/parametermanager/snippets/get_param_version.py @@ -20,6 +20,12 @@ # [START parametermanager_get_param_version] + +# The GetParameterVersion operation retrieves parameter metadata and doesn't +# support high-volume access. To access parameter values at scale, +# see Access a parameter version (https://docs.cloud.google.com/secret-manager/parameter-manager/docs/render-parameter-version). + + def get_param_version( project_id: str, parameter_id: str, version_id: str ) -> parametermanager_v1.ParameterVersion: diff --git a/parametermanager/snippets/noxfile_config.py b/parametermanager/snippets/noxfile_config.py index 8123ee4c7e5..0076e5774d6 100644 --- a/parametermanager/snippets/noxfile_config.py +++ b/parametermanager/snippets/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.11", "3.12"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/parametermanager/snippets/regional_samples/render_regional_param_version.py b/parametermanager/snippets/regional_samples/render_regional_param_version.py index 106a684bc79..5ff7e122df9 100644 --- a/parametermanager/snippets/regional_samples/render_regional_param_version.py +++ b/parametermanager/snippets/regional_samples/render_regional_param_version.py @@ -34,7 +34,8 @@ def render_regional_param_version( location_id (str): The ID of the region where the parameter is located. parameter_id (str): The ID of the parameter for which version details are to be rendered. - version_id (str): The ID of the version to be rendered. + version_id (str): The ID of the version or alias (e.g. "latest") to + be rendered. Returns: parametermanager_v1.RenderParameterVersionResponse: An object diff --git a/parametermanager/snippets/render_param_version.py b/parametermanager/snippets/render_param_version.py index 7a0cefe3298..1c9a8b64618 100644 --- a/parametermanager/snippets/render_param_version.py +++ b/parametermanager/snippets/render_param_version.py @@ -32,7 +32,7 @@ def render_param_version( project_id (str): The ID of the project where the parameter is located. parameter_id (str): The ID of the parameter for which version details are to be rendered. - version_id (str): The ID of the version to be rendered. + version_id (str): The ID of the version or alias (e.g. "latest") to be rendered. Returns: parametermanager_v1.RenderParameterVersionResponse: An object diff --git a/parametermanager/snippets/requirements-test.txt b/parametermanager/snippets/requirements-test.txt index 8807ca968dc..f70a9b5f321 100644 --- a/parametermanager/snippets/requirements-test.txt +++ b/parametermanager/snippets/requirements-test.txt @@ -1,3 +1,3 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" google-cloud-secret-manager==2.21.1 google-cloud-kms==3.2.1 diff --git a/parametermanager/snippets/requirements.txt b/parametermanager/snippets/requirements.txt index 012571b208f..0919a6ec653 100644 --- a/parametermanager/snippets/requirements.txt +++ b/parametermanager/snippets/requirements.txt @@ -1 +1 @@ -google-cloud-parametermanager==0.1.3 +google-cloud-parametermanager==0.1.5 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/geospatial-classification/noxfile_config.py b/people-and-planet-ai/geospatial-classification/noxfile_config.py index aa2149abb0d..b6cf4440b34 100644 --- a/people-and-planet-ai/geospatial-classification/noxfile_config.py +++ b/people-and-planet-ai/geospatial-classification/noxfile_config.py @@ -22,8 +22,8 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ Test only on Python 3.10. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # > ℹ️ Test only on Python 3.14. + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/people-and-planet-ai/geospatial-classification/requirements-test.txt b/people-and-planet-ai/geospatial-classification/requirements-test.txt index 4a9e4d68dd3..8bce7490b76 100644 --- a/people-and-planet-ai/geospatial-classification/requirements-test.txt +++ b/people-and-planet-ai/geospatial-classification/requirements-test.txt @@ -1,2 +1,2 @@ -pytest==8.2.0 +pytest==9.0.3 pytest-xdist==3.3.0 diff --git a/people-and-planet-ai/geospatial-classification/requirements.txt b/people-and-planet-ai/geospatial-classification/requirements.txt index 7c19dad051d..72a9474e2fb 100644 --- a/people-and-planet-ai/geospatial-classification/requirements.txt +++ b/people-and-planet-ai/geospatial-classification/requirements.txt @@ -1,5 +1,5 @@ earthengine-api==1.5.9 folium==0.19.5 -google-cloud-aiplatform==1.47.0 +google-cloud-aiplatform==1.157.0 pandas==2.2.3 -tensorflow==2.12.0 +tensorflow==2.21.0 diff --git a/people-and-planet-ai/geospatial-classification/serving_app/Dockerfile b/people-and-planet-ai/geospatial-classification/serving_app/Dockerfile index 24fe02c4ab2..eca5efbad56 100644 --- a/people-and-planet-ai/geospatial-classification/serving_app/Dockerfile +++ b/people-and-planet-ai/geospatial-classification/serving_app/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.13-slim WORKDIR /app diff --git a/people-and-planet-ai/geospatial-classification/serving_app/requirements.txt b/people-and-planet-ai/geospatial-classification/serving_app/requirements.txt index a96216e7d74..96e211e49ed 100644 --- a/people-and-planet-ai/geospatial-classification/serving_app/requirements.txt +++ b/people-and-planet-ai/geospatial-classification/serving_app/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 -tensorflow==2.12.0 -Werkzeug==3.0.3 +tensorflow==2.21.0 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/people-and-planet-ai/image-classification/noxfile_config.py b/people-and-planet-ai/image-classification/noxfile_config.py index 6048504d2cd..df28cd818d6 100644 --- a/people-and-planet-ai/image-classification/noxfile_config.py +++ b/people-and-planet-ai/image-classification/noxfile_config.py @@ -23,7 +23,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. # NOTE: Apache Beam does not currently support Python 3.12. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/people-and-planet-ai/image-classification/requirements-test.txt b/people-and-planet-ai/image-classification/requirements-test.txt index ddf696ef541..21e891cc05a 100644 --- a/people-and-planet-ai/image-classification/requirements-test.txt +++ b/people-and-planet-ai/image-classification/requirements-test.txt @@ -1,3 +1,3 @@ google-cloud-storage==2.16.0 pytest-xdist==3.5.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/people-and-planet-ai/image-classification/requirements.txt b/people-and-planet-ai/image-classification/requirements.txt index 3a5c78d3764..ccfe52b49a4 100644 --- a/people-and-planet-ai/image-classification/requirements.txt +++ b/people-and-planet-ai/image-classification/requirements.txt @@ -1,3 +1,3 @@ -pillow==10.3.0 +pillow==12.3.0 apache-beam[gcp]==2.55.1 -google-cloud-aiplatform==1.47.0 +google-cloud-aiplatform==1.157.0 diff --git a/people-and-planet-ai/land-cover-classification/e2e_test.py b/people-and-planet-ai/land-cover-classification/e2e_test.py index c1c4aeadf9f..04dcc610126 100644 --- a/people-and-planet-ai/land-cover-classification/e2e_test.py +++ b/people-and-planet-ai/land-cover-classification/e2e_test.py @@ -57,7 +57,7 @@ def data_path(bucket_name: str) -> str: def model_path(bucket_name: str) -> str: # This is a different path than where Vertex AI saves its model. gcs_path = f"gs://{bucket_name}/pretrained-model" - conftest.run_cmd("gsutil", "-m", "cp", "-r", "./pretrained-model", gcs_path) + conftest.run_cmd("gcloud", "storage", "cp", "--recursive", "./pretrained-model", gcs_path) return gcs_path diff --git a/people-and-planet-ai/land-cover-classification/noxfile_config.py b/people-and-planet-ai/land-cover-classification/noxfile_config.py index 228e1eb58f5..6a1ef14ed16 100644 --- a/people-and-planet-ai/land-cover-classification/noxfile_config.py +++ b/people-and-planet-ai/land-cover-classification/noxfile_config.py @@ -22,11 +22,11 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # ℹ️ Test only in Python 3.9 since that's what Dataflow currently supports: + # ℹ️ Test only in Python 3.14 since that's what Dataflow currently supports: # https://cloud.google.com/dataflow/docs/support/beam-runtime-support - # "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.10", "3.11"], + # "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Temp disable until team has time for refactoring tests - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/people-and-planet-ai/land-cover-classification/requirements-test.txt b/people-and-planet-ai/land-cover-classification/requirements-test.txt index 7fbed6e8369..42b0d60cb8d 100644 --- a/people-and-planet-ai/land-cover-classification/requirements-test.txt +++ b/people-and-planet-ai/land-cover-classification/requirements-test.txt @@ -4,4 +4,4 @@ importnb==2023.11.1 ipykernel==6.23.3 nbclient==0.8.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3 diff --git a/people-and-planet-ai/land-cover-classification/requirements.txt b/people-and-planet-ai/land-cover-classification/requirements.txt index e547b7dead5..b7618aabfdb 100644 --- a/people-and-planet-ai/land-cover-classification/requirements.txt +++ b/people-and-planet-ai/land-cover-classification/requirements.txt @@ -1,8 +1,8 @@ # Requirements to run the notebooks. -apache-beam[gcp]==2.46.0 +apache-beam[gcp]==2.74.0 earthengine-api==1.5.9 folium==0.19.5 -google-cloud-aiplatform==1.47.0 +google-cloud-aiplatform==1.157.0 imageio==2.36.1 plotly==5.15.0 -tensorflow==2.12.0 +tensorflow==2.21.0 diff --git a/people-and-planet-ai/land-cover-classification/serving/requirements.txt b/people-and-planet-ai/land-cover-classification/serving/requirements.txt index ecf54b40e4d..10d13aa4a67 100644 --- a/people-and-planet-ai/land-cover-classification/serving/requirements.txt +++ b/people-and-planet-ai/land-cover-classification/serving/requirements.txt @@ -1,6 +1,6 @@ # Requirements for the prediction web service. -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' earthengine-api==1.5.9 gunicorn==23.0.0 -tensorflow==2.12.0 -Werkzeug==3.0.3 +tensorflow==2.21.0 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/people-and-planet-ai/land-cover-classification/setup.py b/people-and-planet-ai/land-cover-classification/setup.py index 0bbc85ba962..77d1719e4e7 100644 --- a/people-and-planet-ai/land-cover-classification/setup.py +++ b/people-and-planet-ai/land-cover-classification/setup.py @@ -20,8 +20,8 @@ url="https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/people-and-planet-ai/land-cover-classification", packages=["serving"], install_requires=[ - "apache-beam[gcp]==2.46.0", + "apache-beam[gcp]==2.74.0", "earthengine-api==1.5.9", - "tensorflow==2.12.0", + "tensorflow==2.21.0", ], ) diff --git a/people-and-planet-ai/timeseries-classification/Dockerfile b/people-and-planet-ai/timeseries-classification/Dockerfile index 086b13d11fd..5ce823f7f22 100644 --- a/people-and-planet-ai/timeseries-classification/Dockerfile +++ b/people-and-planet-ai/timeseries-classification/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM python:3.10-slim +FROM python:3.14-slim WORKDIR /pipeline @@ -25,5 +25,5 @@ RUN pip install --no-cache-dir --upgrade pip \ # Set the entrypoint to Apache Beam SDK worker launcher. # Check this matches the apache-beam version in the requirements.txt -COPY --from=apache/beam_python3.10_sdk:2.62.0 /opt/apache/beam /opt/apache/beam +COPY --from=apache/beam_python3.14_sdk:2.73.0 /opt/apache/beam /opt/apache/beam ENTRYPOINT [ "/opt/apache/beam/boot" ] diff --git a/people-and-planet-ai/timeseries-classification/noxfile_config.py b/people-and-planet-ai/timeseries-classification/noxfile_config.py index f8531486af2..ca7ea47357d 100644 --- a/people-and-planet-ai/timeseries-classification/noxfile_config.py +++ b/people-and-planet-ai/timeseries-classification/noxfile_config.py @@ -22,10 +22,11 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # > ℹ️ Test only on Python 3.10. + # > ℹ️ Test only on Python 3.14. # > The Python version used is defined by the Dockerfile, so it's redundant # > to run multiple tests since they would all be running the same Dockerfile. - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # Note: Docker-based sample, testing only against version specified in Dockerfile (3.14) + "ignored_versions": ["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": True, diff --git a/people-and-planet-ai/timeseries-classification/requirements-test.txt b/people-and-planet-ai/timeseries-classification/requirements-test.txt index c73bf42b80c..f840fc2a86e 100644 --- a/people-and-planet-ai/timeseries-classification/requirements-test.txt +++ b/people-and-planet-ai/timeseries-classification/requirements-test.txt @@ -1,3 +1,3 @@ google-api-python-client==2.131.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/people-and-planet-ai/timeseries-classification/requirements.txt b/people-and-planet-ai/timeseries-classification/requirements.txt index c97c9686726..03aa5074639 100644 --- a/people-and-planet-ai/timeseries-classification/requirements.txt +++ b/people-and-planet-ai/timeseries-classification/requirements.txt @@ -1,7 +1,7 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' apache-beam[gcp]==2.46.0 -google-cloud-aiplatform==1.47.0 +google-cloud-aiplatform==1.157.0 gunicorn==23.0.0 pandas==2.2.3 tensorflow==2.12.1 -Werkzeug==3.0.3 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/people-and-planet-ai/weather-forecasting/notebooks/2-dataset.ipynb b/people-and-planet-ai/weather-forecasting/notebooks/2-dataset.ipynb index d4f505d03bc..969a82e30b2 100644 --- a/people-and-planet-ai/weather-forecasting/notebooks/2-dataset.ipynb +++ b/people-and-planet-ai/weather-forecasting/notebooks/2-dataset.ipynb @@ -700,8 +700,7 @@ }, "outputs": [], "source": [ - "!gsutil ls -lh gs://{bucket}/weather/data-small" - ], + "!gcloud storage ls --long --readable-sizes gs://{bucket}/weather/data-small" ], "id": "F43OAIlrDosG" }, { diff --git a/people-and-planet-ai/weather-forecasting/notebooks/3-training.ipynb b/people-and-planet-ai/weather-forecasting/notebooks/3-training.ipynb index ab637613a91..be0cbe5b72a 100644 --- a/people-and-planet-ai/weather-forecasting/notebooks/3-training.ipynb +++ b/people-and-planet-ai/weather-forecasting/notebooks/3-training.ipynb @@ -3,6 +3,7 @@ { "cell_type": "code", "execution_count": null, + "id": "g4jtzXwEvW2-", "metadata": { "cellView": "form", "id": "g4jtzXwEvW2-" @@ -27,11 +28,11 @@ "# KIND, either express or implied. See the License for the\n", "# specific language governing permissions and limitations\n", "# under the License." - ], - "id": "g4jtzXwEvW2-" + ] }, { "cell_type": "markdown", + "id": "HtysPAVSvcMg", "metadata": { "id": "HtysPAVSvcMg" }, @@ -61,11 +62,11 @@ "* 💰 **Cost estimate**: [a few cents on Vertex AI](https://cloud.google.com/vertex-ai/pricing#custom-trained_models)\n", "\n", "💚 This is one of many **machine learning how-to samples** inspired from **real climate solutions** aired on the [People and Planet AI 🎥 series](https://www.youtube.com/playlist?list=PLIivdWyY5sqI-llB35Dcb187ZG155Rs_7)." - ], - "id": "HtysPAVSvcMg" + ] }, { "cell_type": "markdown", + "id": "RuFZck60B8t-", "metadata": { "id": "RuFZck60B8t-" }, @@ -73,36 +74,41 @@ "# 🎬 Before you begin\n", "\n", "Let's start by cloning the GitHub repository, and installing some dependencies." - ], - "id": "RuFZck60B8t-" + ] }, { "cell_type": "code", - "source": [ - "# Now let's get the code from GitHub and navigate to the sample.\n", - "!git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git\n", - "%cd python-docs-samples/people-and-planet-ai/weather-forecasting" - ], + "execution_count": null, + "id": "W-fPxkYD9FaP", "metadata": { "id": "W-fPxkYD9FaP" }, - "execution_count": null, "outputs": [], - "id": "W-fPxkYD9FaP" + "source": [ + "# Now let's get the code from GitHub and navigate to the sample.\n", + "!git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git\n", + "%cd python-docs-samples/people-and-planet-ai/weather-forecasting" + ] }, { "cell_type": "markdown", - "source": [ - "The [`weather-model`](../serving/weather-model) local package contains the model definition and the training script.\n", - "This ensures we use the same model definition for both training and predictions.\n" - ], + "id": "r5OijZcuInAe", "metadata": { "id": "r5OijZcuInAe" }, - "id": "r5OijZcuInAe" + "source": [ + "The [`weather-model`](../serving/weather-model) local package contains the model definition and the training script.\n", + "This ensures we use the same model definition for both training and predictions.\n" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "AlcsK6pd-x0I", + "metadata": { + "id": "AlcsK6pd-x0I" + }, + "outputs": [], "source": [ "# Upgrade `setuptools` to install packages from pyproject.toml files.\n", "!pip install --quiet --upgrade --no-warn-conflicts pip setuptools\n", @@ -112,16 +118,11 @@ "\n", "# Install the `weather-model` local package.\n", "!pip install google-cloud-aiplatform serving/weather-model" - ], - "metadata": { - "id": "AlcsK6pd-x0I" - }, - "execution_count": null, - "outputs": [], - "id": "AlcsK6pd-x0I" + ] }, { "cell_type": "markdown", + "id": "G75Y6HszxBL8", "metadata": { "id": "G75Y6HszxBL8" }, @@ -132,12 +133,12 @@ "In order to ensure everything runs as expected, we **_must_ restart the runtime**. This allows Colab to load the latest versions of the libraries.\n", "\n", "![\"Runtime\" > \"Restart runtime\"](images/restart-runtime.png)" - ], - "id": "G75Y6HszxBL8" + ] }, { "cell_type": "code", "execution_count": null, + "id": "xGXRHJ9TFs24", "metadata": { "id": "xGXRHJ9TFs24" }, @@ -145,46 +146,46 @@ "source": [ "# Alternatively, restart the runtime by ending the process.\n", "exit()" - ], - "id": "xGXRHJ9TFs24" + ] }, { "cell_type": "markdown", - "source": [ - "After restarting the runtime, let's navigate back into the sample directory." - ], + "id": "WI_vvBpPD4tr", "metadata": { "id": "WI_vvBpPD4tr" }, - "id": "WI_vvBpPD4tr" + "source": [ + "After restarting the runtime, let's navigate back into the sample directory." + ] }, { "cell_type": "code", - "source": [ - "%cd python-docs-samples/people-and-planet-ai/weather-forecasting" - ], + "execution_count": null, + "id": "6fdyXMdlD3cz", "metadata": { - "id": "6fdyXMdlD3cz", "colab": { "base_uri": "https://localhost:8080/" }, + "id": "6fdyXMdlD3cz", "outputId": "457ec966-1f2a-4e76-df1b-62d7d9d77e60" }, - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "[Errno 2] No such file or directory: 'python-docs-samples/people-and-planet-ai/weather-forecasting'\n", "/content/python-docs-samples/people-and-planet-ai/weather-forecasting/python-docs-samples/people-and-planet-ai/weather-forecasting\n" ] } ], - "id": "6fdyXMdlD3cz" + "source": [ + "%cd python-docs-samples/people-and-planet-ai/weather-forecasting" + ] }, { "cell_type": "markdown", + "id": "mHvEEW6oyFGV", "metadata": { "id": "mHvEEW6oyFGV" }, @@ -209,12 +210,12 @@ "\n", "Once you have everything ready, you can go ahead and fill in your Google Cloud resources in the following code cell.\n", "Make sure you run it!" - ], - "id": "mHvEEW6oyFGV" + ] }, { "cell_type": "code", "execution_count": null, + "id": "YMPNUR0pyRvy", "metadata": { "cellView": "form", "id": "YMPNUR0pyRvy" @@ -247,11 +248,11 @@ "\n", "# Set the gcloud project for other gcloud commands.\n", "!gcloud config set project {project}" - ], - "id": "YMPNUR0pyRvy" + ] }, { "cell_type": "markdown", + "id": "02b1b9dd", "metadata": { "id": "02b1b9dd" }, @@ -261,11 +262,11 @@ "We need our model for both training and for prediction.\n", "So we created the local [`weather-model`](../serving/weather-model) module.\n", "It contains [`weather/model.py`](../serving/weather-model/weather/model.py) where the model is defined, and [`weather/trainer.py`](../serving/weather-model/weather/trainer.py) where all the training code lives." - ], - "id": "02b1b9dd" + ] }, { "cell_type": "markdown", + "id": "PY5H3OMjfVAR", "metadata": { "id": "PY5H3OMjfVAR" }, @@ -276,26 +277,29 @@ "Fortunately, Vertex AI uses [Cloud Storage FUSE](https://cloud.google.com/blog/products/ai-machine-learning/cloud-storage-file-system-ai-training) to mount and access Cloud Storage files as if they were local files.\n", "\n", "For now, let's download the data files we created in the [🗄️ **Create the dataset**](https://colab.research.google.com/github/GoogleCloudPlatform/python-docs-samples/blob/main/people-and-planet-ai/weather-forecasting/notebooks/2-dataset.ipynb) notebook to have them locally." - ], - "id": "PY5H3OMjfVAR" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "h_IUpnqvO-sa", + "metadata": { + "id": "h_IUpnqvO-sa" + }, + "outputs": [], "source": [ "data_path_gcs = f\"gs://{bucket}/weather/data\"\n", "\n", "!mkdir -p data-training\n", - "!gsutil -m cp {data_path_gcs}/* data-training" - ], - "metadata": { - "id": "h_IUpnqvO-sa" - }, - "id": "h_IUpnqvO-sa", - "execution_count": null, - "outputs": [] + "!gcloud storage cp {data_path_gcs}/* data-training" + ] }, { "cell_type": "markdown", + "id": "Pl3qbyggO7rR", + "metadata": { + "id": "Pl3qbyggO7rR" + }, "source": [ "First, we need to load the dataset to feed it to the model.\n", "To read a dataset in PyTorch, we could manually instantiate a subclass of `torch.utils.data.Dataset`, but we're going to use [Hugging Face 🤗 Datasets](https://huggingface.co/docs/datasets/main/en/index), which are a high-level interface to use datasets more easily.\n", @@ -307,14 +311,16 @@ "To split the our dataset into training and a testing/validation subsets, we use [`Dataset.train_test_split`](https://huggingface.co/docs/datasets/main/en/package_reference/main_classes#datasets.Dataset.train_test_split).\n", "\n", "In [`weather/trainer.py`](../serving/weather-model/weather/trainer.py) we defined the `read_dataset` function to load our data files, and returns us a 🤗 Dataset with train/test splits." - ], - "metadata": { - "id": "Pl3qbyggO7rR" - }, - "id": "Pl3qbyggO7rR" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "rxwvw7ihacXy", + "metadata": { + "id": "rxwvw7ihacXy" + }, + "outputs": [], "source": [ "from weather.trainer import read_dataset\n", "\n", @@ -323,19 +329,12 @@ "\n", "# Read the dataset with train/test splits.\n", "dataset = read_dataset(data_path, train_test_ratio)" - ], - "metadata": { - "id": "rxwvw7ihacXy" - }, - "id": "rxwvw7ihacXy", - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "code", - "source": [ - "print(dataset)" - ], + "execution_count": null, + "id": "ItTBWR98dByh", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -343,12 +342,10 @@ "id": "ItTBWR98dByh", "outputId": "8c522562-9937-4dc5-e482-8bec3cdba277" }, - "id": "ItTBWR98dByh", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "DatasetDict({\n", " train: Dataset({\n", @@ -362,10 +359,17 @@ "})\n" ] } + ], + "source": [ + "print(dataset)" ] }, { "cell_type": "markdown", + "id": "jnlg80Tl4QLS", + "metadata": { + "id": "jnlg80Tl4QLS" + }, "source": [ "> 💡 For more information on loading data into a 🤗 Dataset, refer to the [Loading data](https://huggingface.co/docs/datasets/main/en/loading) guide.\n", "\n", @@ -374,23 +378,12 @@ "Let's see the shapes of the first training example from the `train` split.\n", "When we access an example, we get an `{'inputs': list, 'labels': list}` dictionary, where each value is a [Python list](https://docs.python.org/3/library/stdtypes.html#list).\n", "We can then convert them into [PyTorch tensors](https://pytorch.org/docs/stable/tensors.html) for further use." - ], - "metadata": { - "id": "jnlg80Tl4QLS" - }, - "id": "jnlg80Tl4QLS" + ] }, { "cell_type": "code", - "source": [ - "import torch\n", - "\n", - "train_dataset = dataset[\"train\"]\n", - "example = train_dataset[0] # random access the first element\n", - "\n", - "print(f\"inputs: {torch.as_tensor(example['inputs']).shape}\")\n", - "print(f\"labels: {torch.as_tensor(example['labels']).shape}\")" - ], + "execution_count": null, + "id": "Ji67MIKQ58Zr", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -398,34 +391,42 @@ "id": "Ji67MIKQ58Zr", "outputId": "7e98452a-2744-4a72-93e8-9a53e1f6b695" }, - "id": "Ji67MIKQ58Zr", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "inputs: torch.Size([5, 5, 52])\n", "labels: torch.Size([5, 5, 2])\n" ] } + ], + "source": [ + "import torch\n", + "\n", + "train_dataset = dataset[\"train\"]\n", + "example = train_dataset[0] # random access the first element\n", + "\n", + "print(f\"inputs: {torch.as_tensor(example['inputs']).shape}\")\n", + "print(f\"labels: {torch.as_tensor(example['labels']).shape}\")" ] }, { "cell_type": "markdown", + "id": "JWFJY1pv7T91", + "metadata": { + "id": "JWFJY1pv7T91" + }, "source": [ "The _inputs_ have the shape `(width, height, num_inputs)`, where each input is the value of an Earth Engine band.\n", "\n", "The _outputs_ have the shape `(width, height, num_outputs)`, where each output is a prediction.\n", "We're predicting for 2 and 6 hours into the future, so we get 2 outputs." - ], - "metadata": { - "id": "JWFJY1pv7T91" - }, - "id": "JWFJY1pv7T91" + ] }, { "cell_type": "markdown", + "id": "oQnLpK0OmutA", "metadata": { "id": "oQnLpK0OmutA" }, @@ -442,22 +443,12 @@ "\n", "We need to calculate the mean and standard deviation for each input, so each band is normalized within its own range.\n", "Both the mean and standard deviation must have the shape `(batch, width, height, num_inputs)`, which allows them to _broadcast_ to any batch size, width and height, as long as the `num_inputs` match." - ], - "id": "oQnLpK0OmutA" + ] }, { "cell_type": "code", - "source": [ - "import numpy as np\n", - "\n", - "# Let's get the mean and standard deviation.\n", - "data = np.array(dataset[\"train\"][\"inputs\"], np.float32)\n", - "mean = data.mean(axis=(0, 1, 2))[None, None, None, :]\n", - "std = data.std(axis=(0, 1, 2))[None, None, None, :]\n", - "\n", - "print(f\"mean: {mean.shape}\")\n", - "print(f\"std: {std.shape}\")" - ], + "execution_count": null, + "id": "YkOwsJBuYIHg", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -465,50 +456,42 @@ "id": "YkOwsJBuYIHg", "outputId": "4a3ed264-5dca-4169-bee9-9e4ecaea5409" }, - "id": "YkOwsJBuYIHg", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "mean: (1, 1, 1, 52)\n", "std: (1, 1, 1, 52)\n" ] } + ], + "source": [ + "import numpy as np\n", + "\n", + "# Let's get the mean and standard deviation.\n", + "data = np.array(dataset[\"train\"][\"inputs\"], np.float32)\n", + "mean = data.mean(axis=(0, 1, 2))[None, None, None, :]\n", + "std = data.std(axis=(0, 1, 2))[None, None, None, :]\n", + "\n", + "print(f\"mean: {mean.shape}\")\n", + "print(f\"std: {std.shape}\")" ] }, { "cell_type": "markdown", - "source": [ - "Let's see how the normalization works for a sample of an example's inputs." - ], + "id": "meHaHpxW-zt5", "metadata": { "id": "meHaHpxW-zt5" }, - "id": "meHaHpxW-zt5" + "source": [ + "Let's see how the normalization works for a sample of an example's inputs." + ] }, { "cell_type": "code", - "source": [ - "import torch\n", - "\n", - "from weather.model import Normalization\n", - "\n", - "normalization = Normalization(mean, std)\n", - "\n", - "sample = lambda x: x[0, 0, 0, 10:15].detach().numpy()\n", - "\n", - "print(f\"mean: {sample(normalization.mean)}\")\n", - "print(f\"std: {sample(normalization.std)}\")\n", - "print(\"-\" * 40)\n", - "\n", - "example = dataset[\"train\"][0]\n", - "example_inputs = torch.as_tensor([example[\"inputs\"]])\n", - "normalized_inputs = normalization(example_inputs)\n", - "print(f\"inputs: {sample(example_inputs)}\")\n", - "print(f\"normalized: {sample(normalized_inputs)}\")" - ], + "execution_count": null, + "id": "EUT8fowo-_Bv", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -516,12 +499,10 @@ "id": "EUT8fowo-_Bv", "outputId": "5fc2a64b-7dde-4803-9d21-34264bcf93f5" }, - "id": "EUT8fowo-_Bv", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "mean: [2202.3132 2355.514 2328.052 2470.9158 2687.0806]\n", "std: [256.82922 324.5936 332.1437 480.68338 351.21927]\n", @@ -530,10 +511,33 @@ "normalized: [0.36088872 0.48826003 0.6200569 0.6305278 0.76852113]\n" ] } + ], + "source": [ + "import torch\n", + "\n", + "from weather.model import Normalization\n", + "\n", + "normalization = Normalization(mean, std)\n", + "\n", + "sample = lambda x: x[0, 0, 0, 10:15].detach().numpy()\n", + "\n", + "print(f\"mean: {sample(normalization.mean)}\")\n", + "print(f\"std: {sample(normalization.std)}\")\n", + "print(\"-\" * 40)\n", + "\n", + "example = dataset[\"train\"][0]\n", + "example_inputs = torch.as_tensor([example[\"inputs\"]])\n", + "normalized_inputs = normalization(example_inputs)\n", + "print(f\"inputs: {sample(example_inputs)}\")\n", + "print(f\"normalized: {sample(normalized_inputs)}\")" ] }, { "cell_type": "markdown", + "id": "Idvef7Id49vE", + "metadata": { + "id": "Idvef7Id49vE" + }, "source": [ "After applying the `Normalization` layer, we get small numbers much closer to the range within -1 and 1, they don't have to be _exactly_ within the range, just close enough.\n", "\n", @@ -542,25 +546,12 @@ "We still want to pass our inputs in a channels-last format and want the predictions back as channels-last for convenience, but we must convert them to channels-first for PyTorch convolutional layers to work.\n", "\n", "In [`weather/model.py`](../serving/weather-model/weather/model.py) we define the `MoveDim` layer, which works similar to [`torch.movedim`](https://pytorch.org/docs/stable/generated/torch.movedim.html) so the model can move the channels dimension as needed.\n" - ], - "metadata": { - "id": "Idvef7Id49vE" - }, - "id": "Idvef7Id49vE" + ] }, { "cell_type": "code", - "source": [ - "from weather.model import MoveDim\n", - "\n", - "# We move the channels/last dimension (-1) to the second index (1),\n", - "# since the first (0) is for the batch dimension.\n", - "to_channels_first = MoveDim(-1, 1)\n", - "channels_first = to_channels_first(normalized_inputs)\n", - "\n", - "print(f\"normalized: {normalized_inputs.shape}\")\n", - "print(f\"channels-first: {channels_first.shape}\")" - ], + "execution_count": null, + "id": "AkrmnehOCuol", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -568,21 +559,34 @@ "id": "AkrmnehOCuol", "outputId": "0a41f8d0-8f61-4946-92e1-67e33e3eddf1" }, - "id": "AkrmnehOCuol", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "normalized: torch.Size([1, 5, 5, 52])\n", "channels-first: torch.Size([1, 52, 5, 5])\n" ] } + ], + "source": [ + "from weather.model import MoveDim\n", + "\n", + "# We move the channels/last dimension (-1) to the second index (1),\n", + "# since the first (0) is for the batch dimension.\n", + "to_channels_first = MoveDim(-1, 1)\n", + "channels_first = to_channels_first(normalized_inputs)\n", + "\n", + "print(f\"normalized: {normalized_inputs.shape}\")\n", + "print(f\"channels-first: {channels_first.shape}\")" ] }, { "cell_type": "markdown", + "id": "6JpbxntkEEtv", + "metadata": { + "id": "6JpbxntkEEtv" + }, "source": [ "The model then passes the data through a\n", "[2D Convolutional layer](https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html) for downsampling, and then through a\n", @@ -590,14 +594,28 @@ "We used a [`ReLU`](https://pytorch.org/docs/stable/generated/torch.nn.ReLU.html) activation function inbetween all hidden layers since it's typically a good general purpose activation function.\n", "\n", "The Conv2D and DeConv2D layers form a very simple Fully Convolutional Network architecture, and since we're using the same _kernel size_ for both we get the same `(width, height)` as outputs." - ], - "metadata": { - "id": "6JpbxntkEEtv" - }, - "id": "6JpbxntkEEtv" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "3Ima73TIEG1z", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "3Ima73TIEG1z", + "outputId": "4929396e-74c2-4cd3-9fdf-31e12117f064" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FCN outputs: torch.Size([1, 128, 5, 5])\n" + ] + } + ], "source": [ "num_inputs = 52\n", "num_hidden1 = 64\n", @@ -613,44 +631,22 @@ "\n", "fcn_outputs = fully_convolutional_layers(channels_first)\n", "print(f\"FCN outputs: {fcn_outputs.shape}\")" - ], - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "3Ima73TIEG1z", - "outputId": "4929396e-74c2-4cd3-9fdf-31e12117f064" - }, - "id": "3Ima73TIEG1z", - "execution_count": null, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "FCN outputs: torch.Size([1, 128, 5, 5])\n" - ] - } ] }, { "cell_type": "markdown", - "source": [ - "Now, let's convert the results back into channels-last format with `MoveDim`." - ], + "id": "TkRDEANqFoLd", "metadata": { "id": "TkRDEANqFoLd" }, - "id": "TkRDEANqFoLd" + "source": [ + "Now, let's convert the results back into channels-last format with `MoveDim`." + ] }, { "cell_type": "code", - "source": [ - "to_channels_last = MoveDim(1, -1)\n", - "channels_last = to_channels_last(fcn_outputs)\n", - "\n", - "print(f\"channels-last: {channels_last.shape}\")" - ], + "execution_count": null, + "id": "-oAMnWtfFuzr", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -658,44 +654,37 @@ "id": "-oAMnWtfFuzr", "outputId": "7a436c24-4ab0-4040-dd6f-cbbf167d03ff" }, - "id": "-oAMnWtfFuzr", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "channels-last: torch.Size([1, 5, 5, 128])\n" ] } + ], + "source": [ + "to_channels_last = MoveDim(1, -1)\n", + "channels_last = to_channels_last(fcn_outputs)\n", + "\n", + "print(f\"channels-last: {channels_last.shape}\")" ] }, { "cell_type": "markdown", - "source": [ - "For the last layer, we use a [`Linear`](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html) layer with the number of outputs we want.\n", - "Since we can't have negative precipitation, we passed the model's outputs through a final `ReLU` activation function." - ], + "id": "7x_OkkNvGabm", "metadata": { "id": "7x_OkkNvGabm" }, - "id": "7x_OkkNvGabm" + "source": [ + "For the last layer, we use a [`Linear`](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html) layer with the number of outputs we want.\n", + "Since we can't have negative precipitation, we passed the model's outputs through a final `ReLU` activation function." + ] }, { "cell_type": "code", - "source": [ - "num_outputs = 2\n", - "\n", - "linear = torch.nn.Linear(num_hidden2, num_outputs)\n", - "relu = torch.nn.ReLU()\n", - "\n", - "with torch.no_grad():\n", - " raw_predictions = linear(channels_last)\n", - " predictions = relu(raw_predictions)\n", - "\n", - "print(f\"predictions: {predictions.shape}\")\n", - "print(predictions[0, 0, 0])" - ], + "execution_count": null, + "id": "q0T8CpEaGJew", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -703,21 +692,33 @@ "id": "q0T8CpEaGJew", "outputId": "227fe92d-2f86-4160-aad9-971d08032a51" }, - "id": "q0T8CpEaGJew", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "predictions: torch.Size([1, 5, 5, 2])\n", "tensor([0.0650, 0.0010])\n" ] } + ], + "source": [ + "num_outputs = 2\n", + "\n", + "linear = torch.nn.Linear(num_hidden2, num_outputs)\n", + "relu = torch.nn.ReLU()\n", + "\n", + "with torch.no_grad():\n", + " raw_predictions = linear(channels_last)\n", + " predictions = relu(raw_predictions)\n", + "\n", + "print(f\"predictions: {predictions.shape}\")\n", + "print(predictions[0, 0, 0])" ] }, { "cell_type": "markdown", + "id": "CDQIGsp24EX9", "metadata": { "id": "CDQIGsp24EX9" }, @@ -734,17 +735,12 @@ "To create a `WeatherModel`, we have to pass it a `WeatherConfig`.\n", "The `WeatherConfig` contains all the model's hyperparameters, and we must also pass the _mean_ and _standard deviation_ from the training dataset for the normalization layer.\n", "We defined `WeatherModel.create` which takes in the training dataset inputs and returns us a `WeatherModel` with the right `WeatherConfig`." - ], - "id": "CDQIGsp24EX9" + ] }, { "cell_type": "code", - "source": [ - "from weather.model import WeatherModel\n", - "\n", - "model = WeatherModel.create(dataset[\"train\"][\"inputs\"])\n", - "print(model)" - ], + "execution_count": null, + "id": "h0bzkGqwo-Ic", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -752,12 +748,10 @@ "id": "h0bzkGqwo-Ic", "outputId": "4a0f8622-e3fd-49cf-9eb2-c9e2777174b0" }, - "id": "h0bzkGqwo-Ic", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "WeatherModel(\n", " (layers): Sequential(\n", @@ -774,42 +768,31 @@ ")\n" ] } + ], + "source": [ + "from weather.model import WeatherModel\n", + "\n", + "model = WeatherModel.create(dataset[\"train\"][\"inputs\"])\n", + "print(model)" ] }, { "cell_type": "markdown", + "id": "6iS60sGCJczT", + "metadata": { + "id": "6iS60sGCJczT" + }, "source": [ "The model outputs a `{'loss': torch.Tensor, 'logits': torch.Tensor}` dictionary during training, and a `{'logits': torch.Tensor}` dictionary during predictions.\n", "This is what 🤗 Transformers expect for [model outputs](https://huggingface.co/docs/transformers/main/en/main_classes/output).\n", "\n", "Remember that we _must_ pass a _batch_ of inputs to the model, not a single input." - ], - "metadata": { - "id": "6iS60sGCJczT" - }, - "id": "6iS60sGCJczT" + ] }, { "cell_type": "code", - "source": [ - "example = dataset[\"test\"]\n", - "inputs_batch = torch.as_tensor(example[\"inputs\"][:1])\n", - "labels_batch = torch.as_tensor(example[\"labels\"][:1])\n", - "\n", - "# We pass the labels as well to get the loss, but it's optional.\n", - "# If we don't pass the labels, we simply won't get the loss.\n", - "# The predictions are under the 'logits' key.\n", - "with torch.no_grad():\n", - " predictions = model(inputs_batch, labels_batch)\n", - "\n", - "print(f\"inputs: {inputs_batch.shape}\")\n", - "print(f\"labels: {labels_batch.shape}\")\n", - "print(f\"loss: {predictions['loss']}\")\n", - "print(f\"predictions: {predictions['logits'].shape}\")\n", - "print(\"-\" * 40)\n", - "print(f\"sample labels: {labels_batch[0, 0, 0]}\")\n", - "print(f\"sample predictions: {predictions['logits'][0, 0, 0]}\")" - ], + "execution_count": null, + "id": "gKwdukOzJeNA", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -817,12 +800,10 @@ "id": "gKwdukOzJeNA", "outputId": "63287630-c21b-4b21-c6b0-168423fd2746" }, - "id": "gKwdukOzJeNA", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "inputs: torch.Size([1, 5, 5, 52])\n", "labels: torch.Size([1, 5, 5, 2])\n", @@ -833,21 +814,44 @@ "sample predictions: tensor([0.0797, 0.0000])\n" ] } + ], + "source": [ + "example = dataset[\"test\"]\n", + "inputs_batch = torch.as_tensor(example[\"inputs\"][:1])\n", + "labels_batch = torch.as_tensor(example[\"labels\"][:1])\n", + "\n", + "# We pass the labels as well to get the loss, but it's optional.\n", + "# If we don't pass the labels, we simply won't get the loss.\n", + "# The predictions are under the 'logits' key.\n", + "with torch.no_grad():\n", + " predictions = model(inputs_batch, labels_batch)\n", + "\n", + "print(f\"inputs: {inputs_batch.shape}\")\n", + "print(f\"labels: {labels_batch.shape}\")\n", + "print(f\"loss: {predictions['loss']}\")\n", + "print(f\"predictions: {predictions['logits'].shape}\")\n", + "print(\"-\" * 40)\n", + "print(f\"sample labels: {labels_batch[0, 0, 0]}\")\n", + "print(f\"sample predictions: {predictions['logits'][0, 0, 0]}\")" ] }, { "cell_type": "markdown", - "source": [ - "These predictions don't look great because we haven't trained our model.\n", - "Fortunately, since we've made our model compatible with 🤗 Transformers, we can simply use [`Trainer`](https://huggingface.co/docs/transformers/main/en/main_classes/trainer), which takes care of all the training steps, automatically optimizes the whole process, and uses accelerators like GPUs if available." - ], + "id": "cxyoRnNlzsYu", "metadata": { "id": "cxyoRnNlzsYu" }, - "id": "cxyoRnNlzsYu" + "source": [ + "These predictions don't look great because we haven't trained our model.\n", + "Fortunately, since we've made our model compatible with 🤗 Transformers, we can simply use [`Trainer`](https://huggingface.co/docs/transformers/main/en/main_classes/trainer), which takes care of all the training steps, automatically optimizes the whole process, and uses accelerators like GPUs if available." + ] }, { "cell_type": "markdown", + "id": "xG6PnXhfLzxO", + "metadata": { + "id": "xG6PnXhfLzxO" + }, "source": [ "## 👟 Train the model\n", "\n", @@ -857,53 +861,24 @@ "\n", "Then we pass the model, the `TrainingArguments`, and the training and testing datasets into the `Trainer`.\n", "Finally we can train the model with [`Trainer.train`](https://huggingface.co/docs/transformers/main/en/main_classes/trainer#transformers.Trainer.train)." - ], - "metadata": { - "id": "xG6PnXhfLzxO" - }, - "id": "xG6PnXhfLzxO" + ] }, { "cell_type": "code", - "source": [ - "from transformers import TrainingArguments, Trainer\n", - "\n", - "epochs = 5\n", - "batch_size = 512\n", - "\n", - "# Define our training job.\n", - "training_args = TrainingArguments(\n", - " output_dir=\"checkpoints\",\n", - " per_device_train_batch_size=batch_size,\n", - " per_device_eval_batch_size=batch_size,\n", - " num_train_epochs=epochs,\n", - " logging_strategy=\"epoch\",\n", - " evaluation_strategy=\"epoch\",\n", - ")\n", - "trainer = Trainer(\n", - " model,\n", - " training_args,\n", - " train_dataset=dataset[\"train\"],\n", - " eval_dataset=dataset[\"test\"],\n", - ")\n", - "\n", - "# Run the training job.\n", - "trainer.train()" - ], + "execution_count": null, + "id": "x4ta1oIsMveF", "metadata": { - "id": "x4ta1oIsMveF", "colab": { "base_uri": "https://localhost:8080/", "height": 825 }, + "id": "x4ta1oIsMveF", "outputId": "3a4e8674-dfa2-4cbf-8445-c3bcccfe4769" }, - "id": "x4ta1oIsMveF", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "PyTorch: setting up devices\n", "The default value for the training argument `--report_to` will change in v5 (from all installed integrations to none). In v5, you will need to use `--report_to all` to get the same behavior as now. You should start updating your code and make this info disappear :-).\n", @@ -919,11 +894,7 @@ ] }, { - "output_type": "display_data", "data": { - "text/plain": [ - "" - ], "text/html": [ "\n", "
        \n", @@ -967,13 +938,17 @@ " \n", " \n", "

        " + ], + "text/plain": [ + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" }, { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "***** Running Evaluation *****\n", " Num examples = 341\n", @@ -998,48 +973,70 @@ ] }, { - "output_type": "execute_result", "data": { "text/plain": [ "TrainOutput(global_step=30, training_loss=1.2740394274393718, metrics={'train_runtime': 23.7216, 'train_samples_per_second': 646.878, 'train_steps_per_second': 1.265, 'total_flos': 0.0, 'train_loss': 1.2740394274393718, 'epoch': 5.0})" ] }, + "execution_count": 31, "metadata": {}, - "execution_count": 31 + "output_type": "execute_result" } + ], + "source": [ + "from transformers import TrainingArguments, Trainer\n", + "\n", + "epochs = 5\n", + "batch_size = 512\n", + "\n", + "# Define our training job.\n", + "training_args = TrainingArguments(\n", + " output_dir=\"checkpoints\",\n", + " per_device_train_batch_size=batch_size,\n", + " per_device_eval_batch_size=batch_size,\n", + " num_train_epochs=epochs,\n", + " logging_strategy=\"epoch\",\n", + " evaluation_strategy=\"epoch\",\n", + ")\n", + "trainer = Trainer(\n", + " model,\n", + " training_args,\n", + " train_dataset=dataset[\"train\"],\n", + " eval_dataset=dataset[\"test\"],\n", + ")\n", + "\n", + "# Run the training job.\n", + "trainer.train()" ] }, { "cell_type": "markdown", - "source": [ - "> 💡 Both losses should go down every epoch, and they should be roughly similar.\n", - "> If the training loss goes down, but the testing loss stays flat or goes up, it might be a sign that the model is [overfitting](https://developers.google.com/machine-learning/crash-course/generalization/peril-of-overfitting), meaning that it's memorizing the training dataset instead of learning to generalize." - ], + "id": "jPFCmhruOvjB", "metadata": { "id": "jPFCmhruOvjB" }, - "id": "jPFCmhruOvjB" + "source": [ + "> 💡 Both losses should go down every epoch, and they should be roughly similar.\n", + "> If the training loss goes down, but the testing loss stays flat or goes up, it might be a sign that the model is [overfitting](https://developers.google.com/machine-learning/crash-course/generalization/peril-of-overfitting), meaning that it's memorizing the training dataset instead of learning to generalize." + ] }, { "cell_type": "markdown", + "id": "_AxB_p2-z4UH", + "metadata": { + "id": "_AxB_p2-z4UH" + }, "source": [ "## 💾 Save and load the model\n", "\n", "After the model has finished training, we can save it with [`Trainer.save_model`](https://huggingface.co/docs/transformers/main/en/main_classes/trainer#transformers.Trainer.save_model).\n", "\n" - ], - "metadata": { - "id": "_AxB_p2-z4UH" - }, - "id": "_AxB_p2-z4UH" + ] }, { "cell_type": "code", - "source": [ - "trainer.save_model(\"model\")\n", - "\n", - "!ls -lh model" - ], + "execution_count": null, + "id": "NPLnvRydOik0", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -1047,12 +1044,10 @@ "id": "NPLnvRydOik0", "outputId": "c788cf33-ec67-4612-9f44-1262dc872625" }, - "id": "NPLnvRydOik0", - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "Saving model checkpoint to model\n", "Configuration saved in model/config.json\n", @@ -1060,8 +1055,8 @@ ] }, { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "total 420K\n", "-rw-r--r-- 1 root root 3.4K Jan 11 21:33 config.json\n", @@ -1069,10 +1064,16 @@ "-rw-r--r-- 1 root root 3.4K Jan 11 21:33 training_args.bin\n" ] } + ], + "source": [ + "trainer.save_model(\"model\")\n", + "\n", + "!ls -lh model" ] }, { "cell_type": "markdown", + "id": "icxhkboQA_o5", "metadata": { "id": "icxhkboQA_o5" }, @@ -1080,12 +1081,12 @@ "Now that we have a trained model, we can save it and load it anywhere else.\n", "We can load a 🤗 Transformers model with [`PreTrainedModel.from_pretrained`](https://huggingface.co/docs/transformers/main/en/main_classes/model#transformers.PreTrainedModel.from_pretrained), in our case with `WeatherModel.from_pretrained`.\n", "This loads all the model's hyperparameters as well as the _mean_ and _standard deviation_ for the normalization layer." - ], - "id": "icxhkboQA_o5" + ] }, { "cell_type": "code", "execution_count": null, + "id": "xsxX2Mb-CwWk", "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -1095,8 +1096,8 @@ }, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "loading configuration file model/config.json\n", "Model config WeatherConfig {\n", @@ -1244,8 +1245,8 @@ ] }, { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "WeatherModel(\n", " (layers): Sequential(\n", @@ -1268,11 +1269,11 @@ "\n", "model = WeatherModel.from_pretrained(\"model\")\n", "print(model)" - ], - "id": "xsxX2Mb-CwWk" + ] }, { "cell_type": "markdown", + "id": "IO73AYtsCIQ_", "metadata": { "id": "IO73AYtsCIQ_" }, @@ -1290,39 +1291,36 @@ "\n", "The model and trainer are defined in the [`serving/weather-model`](../serving/weather-model) module.\n", "To run it in Vertex AI, we must build the package, copy it to Cloud Storage, and launch a custom training job with [`CustomPythonPackageTrainingJob`](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.CustomPythonPackageTrainingJob)." - ], - "id": "IO73AYtsCIQ_" + ] }, { "cell_type": "code", - "source": [ - "# Build the `weather-model` package.\n", - "!python -m build serving/weather-model" - ], + "execution_count": null, + "id": "v1SZt1iA2Wrh", "metadata": { "id": "v1SZt1iA2Wrh" }, - "execution_count": null, "outputs": [], - "id": "v1SZt1iA2Wrh" + "source": [ + "# Build the `weather-model` package.\n", + "!python -m build serving/weather-model" + ] }, { "cell_type": "code", - "source": [ - "!ls -lh serving/weather-model/dist" - ], + "execution_count": null, + "id": "y4F1_eA32Wrh", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, - "outputId": "2f2b4dc2-a287-4822-caed-7f8115246d7d", - "id": "y4F1_eA32Wrh" + "id": "y4F1_eA32Wrh", + "outputId": "2f2b4dc2-a287-4822-caed-7f8115246d7d" }, - "execution_count": null, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "total 16K\n", "-rw-r--r-- 1 root root 5.9K Jan 11 18:29 weather_model-1.0.0-py3-none-any.whl\n", @@ -1330,36 +1328,39 @@ ] } ], - "id": "y4F1_eA32Wrh" + "source": [ + "!ls -lh serving/weather-model/dist" + ] }, { "cell_type": "code", - "source": [ - "# Stage the `weather-model` package in Cloud Storage.\n", - "!gsutil cp serving/weather-model/dist/weather-model-1.0.0.tar.gz gs://{bucket}/weather/" - ], + "execution_count": null, + "id": "JA1k9ky02dsx", "metadata": { "id": "JA1k9ky02dsx" }, - "id": "JA1k9ky02dsx", - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "# Stage the `weather-model` package in Cloud Storage.\n", + "!gcloud storage cp serving/weather-model/dist/weather-model-1.0.0.tar.gz gs://{bucket}/weather/" + ] }, { "cell_type": "markdown", + "id": "yk9X4YQcDPpR", + "metadata": { + "id": "yk9X4YQcDPpR" + }, "source": [ "In Vertex AI, we can access Cloud Storage files directly as if they were local files via Cloud Storage FUSE.\n", "Cloud Storage files are available under `/gcs` followed by your bucket and file path.\n", "To learn more, see the [Cloud Storage as a File System in AI Training](https://cloud.google.com/blog/products/ai-machine-learning/cloud-storage-file-system-ai-training) blog post." - ], - "metadata": { - "id": "yk9X4YQcDPpR" - }, - "id": "yk9X4YQcDPpR" + ] }, { "cell_type": "code", "execution_count": null, + "id": "Ny4x99GiS2Lm", "metadata": { "id": "Ny4x99GiS2Lm" }, @@ -1381,7 +1382,7 @@ " display_name=\"weather-forecasting\",\n", " python_package_gcs_uri=f\"gs://{bucket}/weather/weather-model-1.0.0.tar.gz\",\n", " python_module_name=\"weather.trainer\",\n", - " container_uri=\"us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.2-8.py310:latest\",\n", + " container_uri=\"us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.2-4.py310:latest\",\n", ")\n", "job.run(\n", " machine_type=\"n1-highmem-8\",\n", @@ -1394,31 +1395,30 @@ " ],\n", " timeout=timeout_min * 60, # in seconds\n", ")" - ], - "id": "Ny4x99GiS2Lm" + ] }, { "cell_type": "markdown", + "id": "zw_kcyw4gOLF", "metadata": { "id": "zw_kcyw4gOLF" }, "source": [ "> 💡 Look at your Vertex AI training jobs: https://console.cloud.google.com/vertex-ai/training/custom-jobs" - ], - "id": "zw_kcyw4gOLF" + ] }, { "cell_type": "markdown", + "id": "79RnF-lYBRTS", + "metadata": { + "id": "79RnF-lYBRTS" + }, "source": [ "# ⛳️ What's next?\n", "\n", "* [![Open in Colab](https://github.com/googlecolab/open_in_colab/raw/main/images/icon16.png) **🔮 Model predictions**](https://colab.research.google.com/github/GoogleCloudPlatform/python-docs-samples/blob/main/people-and-planet-ai/weather-forecasting/notebooks/4-predictions.ipynb):\n", " Get predictions from the model with data it has never seen before." - ], - "metadata": { - "id": "79RnF-lYBRTS" - }, - "id": "79RnF-lYBRTS" + ] } ], "metadata": { diff --git a/people-and-planet-ai/weather-forecasting/notebooks/4-predictions.ipynb b/people-and-planet-ai/weather-forecasting/notebooks/4-predictions.ipynb index a2d72385465..405b52a5bd3 100644 --- a/people-and-planet-ai/weather-forecasting/notebooks/4-predictions.ipynb +++ b/people-and-planet-ai/weather-forecasting/notebooks/4-predictions.ipynb @@ -336,8 +336,7 @@ "model_path_gcs = f\"gs://{bucket}/weather/model\"\n", "\n", "!mkdir -p model\n", - "!gsutil cp {model_path_gcs}/* model" - ], + "!gcloud storage cp {model_path_gcs}/* model" ], "metadata": { "id": "5w_uNjluhDMG" }, diff --git a/people-and-planet-ai/weather-forecasting/serving/Dockerfile.backup b/people-and-planet-ai/weather-forecasting/serving/Dockerfile.backup index 2ee37566e48..47113bf3d88 100644 --- a/people-and-planet-ai/weather-forecasting/serving/Dockerfile.backup +++ b/people-and-planet-ai/weather-forecasting/serving/Dockerfile.backup @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM python:slim +FROM python:3.14-slim WORKDIR /app COPY . ./ diff --git a/people-and-planet-ai/weather-forecasting/serving/requirements.txt b/people-and-planet-ai/weather-forecasting/serving/requirements.txt index a862608066a..7fdab058bce 100644 --- a/people-and-planet-ai/weather-forecasting/serving/requirements.txt +++ b/people-and-planet-ai/weather-forecasting/serving/requirements.txt @@ -1,7 +1,6 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 -Werkzeug==3.0.3 - +Werkzeug==3.1.8; python_version >= '3.9' # Local packages. ./weather-data ./weather-model 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..b15bface40d 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 @@ -18,8 +18,8 @@ name = "weather-model" 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", + "torch==2.4.0", # make sure this matches the `container_uri` in `notebooks/3-training.ipynb` + "transformers==5.13.0", ] [project.scripts] diff --git a/people-and-planet-ai/weather-forecasting/tests/dataset_tests/noxfile_config.py b/people-and-planet-ai/weather-forecasting/tests/dataset_tests/noxfile_config.py index 81b47f2ab34..16d3006025f 100644 --- a/people-and-planet-ai/weather-forecasting/tests/dataset_tests/noxfile_config.py +++ b/people-and-planet-ai/weather-forecasting/tests/dataset_tests/noxfile_config.py @@ -22,10 +22,10 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # 💡 Only test with Python 3.10 - # "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11"], + # 💡 Only test with Python 3.14 + # "ignored_versions": ["3.8", "3.9", "3.11", "3.12", "3.13"], # Temp disable until team has time for refactoring tests - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/people-and-planet-ai/weather-forecasting/tests/dataset_tests/requirements-test.txt b/people-and-planet-ai/weather-forecasting/tests/dataset_tests/requirements-test.txt index f4245b7398e..15381c30591 100644 --- a/people-and-planet-ai/weather-forecasting/tests/dataset_tests/requirements-test.txt +++ b/people-and-planet-ai/weather-forecasting/tests/dataset_tests/requirements-test.txt @@ -1,4 +1,4 @@ ipykernel==6.23.3 nbclient==0.8.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/people-and-planet-ai/weather-forecasting/tests/overview_tests/noxfile_config.py b/people-and-planet-ai/weather-forecasting/tests/overview_tests/noxfile_config.py index 08cdcbbe707..694f7e54512 100644 --- a/people-and-planet-ai/weather-forecasting/tests/overview_tests/noxfile_config.py +++ b/people-and-planet-ai/weather-forecasting/tests/overview_tests/noxfile_config.py @@ -22,8 +22,8 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # 💡 Only test with Python 3.10 - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # 💡 Only test with Python 3.14 + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/people-and-planet-ai/weather-forecasting/tests/overview_tests/requirements-test.txt b/people-and-planet-ai/weather-forecasting/tests/overview_tests/requirements-test.txt index f4245b7398e..15381c30591 100644 --- a/people-and-planet-ai/weather-forecasting/tests/overview_tests/requirements-test.txt +++ b/people-and-planet-ai/weather-forecasting/tests/overview_tests/requirements-test.txt @@ -1,4 +1,4 @@ ipykernel==6.23.3 nbclient==0.8.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/people-and-planet-ai/weather-forecasting/tests/predictions_tests/noxfile_config.py b/people-and-planet-ai/weather-forecasting/tests/predictions_tests/noxfile_config.py index 08cdcbbe707..694f7e54512 100644 --- a/people-and-planet-ai/weather-forecasting/tests/predictions_tests/noxfile_config.py +++ b/people-and-planet-ai/weather-forecasting/tests/predictions_tests/noxfile_config.py @@ -22,8 +22,8 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # 💡 Only test with Python 3.10 - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # 💡 Only test with Python 3.14 + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/people-and-planet-ai/weather-forecasting/tests/predictions_tests/requirements-test.txt b/people-and-planet-ai/weather-forecasting/tests/predictions_tests/requirements-test.txt index f4245b7398e..15381c30591 100644 --- a/people-and-planet-ai/weather-forecasting/tests/predictions_tests/requirements-test.txt +++ b/people-and-planet-ai/weather-forecasting/tests/predictions_tests/requirements-test.txt @@ -1,4 +1,4 @@ ipykernel==6.23.3 nbclient==0.8.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/people-and-planet-ai/weather-forecasting/tests/predictions_tests/test_predictions.py b/people-and-planet-ai/weather-forecasting/tests/predictions_tests/test_predictions.py index 9e3f63d7949..2f4b5b90e3f 100644 --- a/people-and-planet-ai/weather-forecasting/tests/predictions_tests/test_predictions.py +++ b/people-and-planet-ai/weather-forecasting/tests/predictions_tests/test_predictions.py @@ -37,7 +37,7 @@ def test_name() -> str: @pytest.fixture(scope="session") def model_path_gcs(bucket_name: str) -> str: path_gcs = f"gs://{bucket_name}/model" - conftest.run_cmd("gsutil", "cp", "serving/model/*", path_gcs) + conftest.run_cmd("gcloud", "storage", "cp", "serving/model/*", path_gcs) return path_gcs diff --git a/people-and-planet-ai/weather-forecasting/tests/training_tests/noxfile_config.py b/people-and-planet-ai/weather-forecasting/tests/training_tests/noxfile_config.py index 08cdcbbe707..694f7e54512 100644 --- a/people-and-planet-ai/weather-forecasting/tests/training_tests/noxfile_config.py +++ b/people-and-planet-ai/weather-forecasting/tests/training_tests/noxfile_config.py @@ -22,8 +22,8 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. - # 💡 Only test with Python 3.10 - "ignored_versions": ["2.7", "3.6", "3.7", "3.8", "3.9", "3.11", "3.12", "3.13"], + # 💡 Only test with Python 3.14 + "ignored_versions": ["3.8", "3.9", "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": True, diff --git a/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements-test.txt b/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements-test.txt index 316d7ea45ab..94ea22e57d3 100644 --- a/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements-test.txt +++ b/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements-test.txt @@ -2,4 +2,4 @@ ipykernel==6.23.3 nbclient==0.8.0 pytest-xdist==3.3.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements.txt b/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements.txt index 2dda40903c2..d2b1b4ba08d 100644 --- a/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements.txt +++ b/people-and-planet-ai/weather-forecasting/tests/training_tests/requirements.txt @@ -1,3 +1,3 @@ ../../serving/weather-model build==0.10.0 -google-cloud-aiplatform==1.47.0 +google-cloud-aiplatform==1.157.0 diff --git a/people-and-planet-ai/weather-forecasting/tests/training_tests/test_training.py b/people-and-planet-ai/weather-forecasting/tests/training_tests/test_training.py index 1f921794ec6..140aef9758a 100644 --- a/people-and-planet-ai/weather-forecasting/tests/training_tests/test_training.py +++ b/people-and-planet-ai/weather-forecasting/tests/training_tests/test_training.py @@ -51,7 +51,7 @@ def data_path_gcs(bucket_name: str) -> str: inputs_batch = [inputs] * batch_size labels_batch = [labels] * batch_size np.savez_compressed(f, inputs=inputs_batch, labels=labels_batch) - conftest.run_cmd("gsutil", "cp", f.name, f"{path_gcs}/example.npz") + conftest.run_cmd("gcloud", "storage", "cp", f.name, f"{path_gcs}/example.npz") return path_gcs diff --git a/practice-folder/beginner-sample/requirements-test.txt b/practice-folder/beginner-sample/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/practice-folder/beginner-sample/requirements-test.txt +++ b/practice-folder/beginner-sample/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/practice-folder/beginner-sample/requirements.txt b/practice-folder/beginner-sample/requirements.txt index 95fef4eb661..b3ff32e568d 100644 --- a/practice-folder/beginner-sample/requirements.txt +++ b/practice-folder/beginner-sample/requirements.txt @@ -1 +1 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' diff --git a/practice-folder/intermediate-sample/requirements-test.txt b/practice-folder/intermediate-sample/requirements-test.txt index 73e69569a52..dc96a119d19 100644 --- a/practice-folder/intermediate-sample/requirements-test.txt +++ b/practice-folder/intermediate-sample/requirements-test.txt @@ -1,2 +1,2 @@ google-cloud-storage==2.13.0 -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/practice-folder/noxfile_config.py b/practice-folder/noxfile_config.py index 8809d995c26..b7d8115d773 100644 --- a/practice-folder/noxfile_config.py +++ b/practice-folder/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": ["3.8", "3.9", "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": True, diff --git a/privateca/snippets/noxfile_config.py b/privateca/snippets/noxfile_config.py index 5125a5da533..b464de8ca06 100644 --- a/privateca/snippets/noxfile_config.py +++ b/privateca/snippets/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"], + "ignored_versions": ["3.8", "3.9", "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/privateca/snippets/requirements-test.txt b/privateca/snippets/requirements-test.txt index bfeffa644e9..5b2a71ccd94 100644 --- a/privateca/snippets/requirements-test.txt +++ b/privateca/snippets/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==8.2.0 +pytest==9.0.3 google-auth==2.38.0 -cryptography==45.0.1 +cryptography==48.0.1 backoff==2.2.1 \ No newline at end of file diff --git a/profiler/appengine/flexible/noxfile_config.py b/profiler/appengine/flexible/noxfile_config.py index 5c4a4b39a25..3e4fbc74766 100644 --- a/profiler/appengine/flexible/noxfile_config.py +++ b/profiler/appengine/flexible/noxfile_config.py @@ -23,7 +23,7 @@ TEST_CONFIG_OVERRIDE = { # You can opt out from the test for specific Python versions. # 3.11 is currently unsupported - "ignored_versions": ["2.7", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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/profiler/appengine/flexible/requirements-test.txt b/profiler/appengine/flexible/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/profiler/appengine/flexible/requirements-test.txt +++ b/profiler/appengine/flexible/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/profiler/appengine/flexible/requirements.txt b/profiler/appengine/flexible/requirements.txt index 31fd601964e..5377b69afca 100644 --- a/profiler/appengine/flexible/requirements.txt +++ b/profiler/appengine/flexible/requirements.txt @@ -1,4 +1,4 @@ -Flask==3.0.3 +Flask==3.1.3; python_version >= '3.9' gunicorn==23.0.0 google-cloud-profiler==4.1.0 -Werkzeug==3.0.3 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/profiler/appengine/standard_python37/noxfile_config.py b/profiler/appengine/standard_python37/noxfile_config.py index f61d176ce12..f27eeab556e 100644 --- a/profiler/appengine/standard_python37/noxfile_config.py +++ b/profiler/appengine/standard_python37/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.6", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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/profiler/appengine/standard_python37/requirements-test.txt b/profiler/appengine/standard_python37/requirements-test.txt index c021c5b5b70..c9e154ba440 100644 --- a/profiler/appengine/standard_python37/requirements-test.txt +++ b/profiler/appengine/standard_python37/requirements-test.txt @@ -1 +1 @@ -pytest==7.2.2 +pytest==9.0.3; python_version >= "3.10" diff --git a/profiler/appengine/standard_python37/requirements.txt b/profiler/appengine/standard_python37/requirements.txt index b4e3738644e..8f0ccea3617 100644 --- a/profiler/appengine/standard_python37/requirements.txt +++ b/profiler/appengine/standard_python37/requirements.txt @@ -1,3 +1,3 @@ -Flask==3.0.0 +Flask==3.1.3; python_version >= '3.9' google-cloud-profiler==4.0.0 -Werkzeug==3.0.3 +Werkzeug==3.1.8; python_version >= '3.9' diff --git a/profiler/quickstart/noxfile_config.py b/profiler/quickstart/noxfile_config.py index aef7b534168..f27eeab556e 100644 --- a/profiler/quickstart/noxfile_config.py +++ b/profiler/quickstart/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.11"], + "ignored_versions": ["3.8", "3.9", "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/profiler/quickstart/requirements-test.txt b/profiler/quickstart/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/profiler/quickstart/requirements-test.txt +++ b/profiler/quickstart/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/pubsub/streaming-analytics/PubSubToGCS_test.py b/pubsub/streaming-analytics/PubSubToGCS_test.py index 35fd9fa5a89..8ce4e0bff2d 100644 --- a/pubsub/streaming-analytics/PubSubToGCS_test.py +++ b/pubsub/streaming-analytics/PubSubToGCS_test.py @@ -61,7 +61,7 @@ def test_pubsub_to_gcs(): # Check for output files on GCS. gcs_client = GcsIO() - files = gcs_client.list_prefix(f"gs://{BUCKET}/pubsub/{UUID}") + files = dict(gcs_client.list_files(f"gs://{BUCKET}/pubsub/{UUID}")) assert len(files) > 0 # Clean up. diff --git a/pubsub/streaming-analytics/README.md b/pubsub/streaming-analytics/README.md index 91f0cd25965..f22a6ae5a2b 100644 --- a/pubsub/streaming-analytics/README.md +++ b/pubsub/streaming-analytics/README.md @@ -26,7 +26,7 @@ Sample(s) showing how to use [Google Cloud Pub/Sub] with [Google Cloud Dataflow] gcloud init ``` -1. [Enable the APIs](https://console.cloud.google.com/flows/enableapi?apiid=dataflow,compute_component,logging,storage_component,storage_api,pubsub,cloudresourcemanager.googleapis.com,cloudscheduler.googleapis.com,appengine.googleapis.com): Dataflow, Compute Engine, Stackdriver Logging, Cloud Storage, Cloud Storage JSON, Pub/Sub, Cloud Scheduler, Cloud Resource Manager, and App Engine. +1. [Enable the APIs](https://console.cloud.google.com/flows/enableapi?apiid=dataflow,compute_component,logging,storage_component,storage_api,pubsub,cloudresourcemanager.googleapis.com,cloudscheduler.googleapis.com,appengine.googleapis.com): Dataflow, Compute Engine, Cloud Logging, Cloud Storage, Cloud Storage JSON, Pub/Sub, Cloud Scheduler, Cloud Resource Manager, and App Engine. 1. Create a service account JSON key via the [*Create service account key* page], @@ -80,7 +80,7 @@ Sample(s) showing how to use [Google Cloud Pub/Sub] with [Google Cloud Dataflow] ```bash export BUCKET_ID=your-gcs-bucket-id - gsutil mb gs://$BUCKET_ID + gcloud storage buckets create gs://$BUCKET_ID ``` 1. Start a [Google Cloud Scheduler] job that publishes one message to a [Google Cloud Pub/Sub] topic every minute. This will create an [App Engine] app if one has never been created on the project. @@ -173,7 +173,7 @@ After the job has been submitted, you can check its status in the [GCP Console D You can also check the output to your GCS bucket using the command line below or in the [GCP Console Storage page]. You may need to wait a few minutes for the files to appear. ```bash -gsutil ls gs://$BUCKET_ID/samples/ +gcloud storage ls gs://$BUCKET_ID/samples/ ``` ## Cleanup @@ -198,10 +198,10 @@ gsutil ls gs://$BUCKET_ID/samples/ ```bash # Delete only the files created by this sample. - gsutil -m rm -rf "gs://$BUCKET_ID/samples/output*" + gcloud storage rm --recursive --continue-on-error "gs://$BUCKET_ID/samples/output*" # [optional] Remove the Cloud Storage bucket. - gsutil rb gs://$BUCKET_ID + gcloud storage buckets delete gs://$BUCKET_ID ``` [Apache Beam]: https://beam.apache.org/ diff --git a/pubsub/streaming-analytics/noxfile_config.py b/pubsub/streaming-analytics/noxfile_config.py index 783945807ee..932bef266b6 100644 --- a/pubsub/streaming-analytics/noxfile_config.py +++ b/pubsub/streaming-analytics/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.6", "3.9", "3.10", "3.11", "3.12", "3.13"], + "ignored_versions": ["3.8", "3.9", "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/pubsub/streaming-analytics/requirements-test.txt b/pubsub/streaming-analytics/requirements-test.txt index 15d066af319..c9e154ba440 100644 --- a/pubsub/streaming-analytics/requirements-test.txt +++ b/pubsub/streaming-analytics/requirements-test.txt @@ -1 +1 @@ -pytest==8.2.0 +pytest==9.0.3; python_version >= "3.10" diff --git a/pubsub/streaming-analytics/requirements.txt b/pubsub/streaming-analytics/requirements.txt index 7dac644ad0f..44955c079c5 100644 --- a/pubsub/streaming-analytics/requirements.txt +++ b/pubsub/streaming-analytics/requirements.txt @@ -1 +1 @@ -apache-beam[gcp,test]==2.42.0 +apache-beam[gcp,test]==2.72.0 diff --git a/pubsublite/spark-connector/README.md b/pubsublite/spark-connector/README.md index dc800440166..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 @@ -193,7 +193,7 @@ Here is an example output: