diff --git a/.gitignore b/.gitignore index 72627b4..fb5894a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,8 +50,10 @@ docs.metadata # Virtual environment env/ + +# Test logs coverage.xml -sponge_log.xml +*sponge_log.xml # System test environment variables. system_tests/local_test_setup diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 520b1ff..fa307d6 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -40,6 +40,16 @@ python3 -m pip uninstall --yes --quiet nox-automation python3 -m pip install --upgrade --quiet nox python3 -m nox --version +# If this is a continuous build, send the test log to the FlakyBot. +# See https://github.com/googleapis/repo-automation-bots/tree/master/packages/flakybot. +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then + cleanup() { + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot + } + trap cleanup EXIT HUP +fi + # If NOX_SESSION is set, it only runs the specified session, # otherwise run all the sessions. if [[ -n "${NOX_SESSION:-}" ]]; then diff --git a/.kokoro/samples/python3.6/periodic-head.cfg b/.kokoro/samples/python3.6/periodic-head.cfg new file mode 100644 index 0000000..f9cfcd3 --- /dev/null +++ b/.kokoro/samples/python3.6/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.7/periodic-head.cfg b/.kokoro/samples/python3.7/periodic-head.cfg new file mode 100644 index 0000000..f9cfcd3 --- /dev/null +++ b/.kokoro/samples/python3.7/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.8/periodic-head.cfg b/.kokoro/samples/python3.8/periodic-head.cfg new file mode 100644 index 0000000..f9cfcd3 --- /dev/null +++ b/.kokoro/samples/python3.8/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-pubsub/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/test-samples-against-head.sh b/.kokoro/test-samples-against-head.sh new file mode 100755 index 0000000..f7cd4f4 --- /dev/null +++ b/.kokoro/test-samples-against-head.sh @@ -0,0 +1,28 @@ +#!/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 +# +# 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. + +# A customized test runner for samples. +# +# For periodic builds, you can specify this file for testing against head. + +# `-e` enables the script to automatically fail when a command fails +# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero +set -eo pipefail +# Enables `**` to include files nested inside sub-folders +shopt -s globstar + +cd github/python-billing + +exec .kokoro/test-samples-impl.sh diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh new file mode 100755 index 0000000..cf5de74 --- /dev/null +++ b/.kokoro/test-samples-impl.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# 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. + + +# `-e` enables the script to automatically fail when a command fails +# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero +set -eo pipefail +# Enables `**` to include files nested inside sub-folders +shopt -s globstar + +# Exit early if samples directory doesn't exist +if [ ! -d "./samples" ]; then + echo "No tests run. `./samples` not found" + exit 0 +fi + +# Disable buffering, so that the logs stream through. +export PYTHONUNBUFFERED=1 + +# Debug: show build environment +env | grep KOKORO + +# Install nox +python3.6 -m pip install --upgrade --quiet nox + +# Use secrets acessor service account to get secrets +if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then + gcloud auth activate-service-account \ + --key-file="${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" \ + --project="cloud-devrel-kokoro-resources" +fi + +# This script will create 3 files: +# - testing/test-env.sh +# - testing/service-account.json +# - testing/client-secrets.json +./scripts/decrypt-secrets.sh + +source ./testing/test-env.sh +export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/testing/service-account.json + +# For cloud-run session, we activate the service account for gcloud sdk. +gcloud auth activate-service-account \ + --key-file "${GOOGLE_APPLICATION_CREDENTIALS}" + +export GOOGLE_CLIENT_SECRETS=$(pwd)/testing/client-secrets.json + +echo -e "\n******************** TESTING PROJECTS ********************" + +# Switch to 'fail at end' to allow all tests to complete before exiting. +set +e +# Use RTN to return a non-zero value if the test fails. +RTN=0 +ROOT=$(pwd) +# Find all requirements.txt in the samples directory (may break on whitespace). +for file in samples/**/requirements.txt; do + cd "$ROOT" + # Navigate to the project folder. + file=$(dirname "$file") + cd "$file" + + echo "------------------------------------------------------------" + echo "- testing $file" + echo "------------------------------------------------------------" + + # Use nox to execute the tests for the project. + python3.6 -m nox -s "$RUN_TESTS_SESSION" + EXIT=$? + + # If this is a periodic build, send the test log to the FlakyBot. + # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/flakybot. + if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot + fi + + if [[ $EXIT -ne 0 ]]; then + RTN=1 + echo -e "\n Testing failed: Nox returned a non-zero exit code. \n" + else + echo -e "\n Testing completed.\n" + fi + +done +cd "$ROOT" + +# Workaround for Kokoro permissions issue: delete secrets +rm testing/{test-env.sh,client-secrets.json,service-account.json} + +exit "$RTN" diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh index bb9219e..650a7a2 100755 --- a/.kokoro/test-samples.sh +++ b/.kokoro/test-samples.sh @@ -13,6 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +# The default test runner for samples. +# +# For periodic builds, we rewinds the repo to the latest release, and +# run test-samples-impl.sh. # `-e` enables the script to automatically fail when a command fails # `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero @@ -24,87 +28,19 @@ cd github/python-billing # Run periodic samples tests at latest release if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then + # preserving the test runner implementation. + cp .kokoro/test-samples-impl.sh "${TMPDIR}/test-samples-impl.sh" + echo "--- IMPORTANT IMPORTANT IMPORTANT ---" + echo "Now we rewind the repo back to the latest release..." LATEST_RELEASE=$(git describe --abbrev=0 --tags) git checkout $LATEST_RELEASE -fi - -# Exit early if samples directory doesn't exist -if [ ! -d "./samples" ]; then - echo "No tests run. `./samples` not found" - exit 0 -fi - -# Disable buffering, so that the logs stream through. -export PYTHONUNBUFFERED=1 - -# Debug: show build environment -env | grep KOKORO - -# Install nox -python3.6 -m pip install --upgrade --quiet nox - -# Use secrets acessor service account to get secrets -if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then - gcloud auth activate-service-account \ - --key-file="${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" \ - --project="cloud-devrel-kokoro-resources" -fi - -# This script will create 3 files: -# - testing/test-env.sh -# - testing/service-account.json -# - testing/client-secrets.json -./scripts/decrypt-secrets.sh - -source ./testing/test-env.sh -export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/testing/service-account.json - -# For cloud-run session, we activate the service account for gcloud sdk. -gcloud auth activate-service-account \ - --key-file "${GOOGLE_APPLICATION_CREDENTIALS}" - -export GOOGLE_CLIENT_SECRETS=$(pwd)/testing/client-secrets.json - -echo -e "\n******************** TESTING PROJECTS ********************" - -# Switch to 'fail at end' to allow all tests to complete before exiting. -set +e -# Use RTN to return a non-zero value if the test fails. -RTN=0 -ROOT=$(pwd) -# Find all requirements.txt in the samples directory (may break on whitespace). -for file in samples/**/requirements.txt; do - cd "$ROOT" - # Navigate to the project folder. - file=$(dirname "$file") - cd "$file" - - echo "------------------------------------------------------------" - echo "- testing $file" - echo "------------------------------------------------------------" - - # Use nox to execute the tests for the project. - python3.6 -m nox -s "$RUN_TESTS_SESSION" - EXIT=$? - - # If this is a periodic build, send the test log to the FlakyBot. - # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/flakybot. - if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then - chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot - $KOKORO_GFILE_DIR/linux_amd64/flakybot + echo "The current head is: " + echo $(git rev-parse --verify HEAD) + echo "--- IMPORTANT IMPORTANT IMPORTANT ---" + # move back the test runner implementation if there's no file. + if [ ! -f .kokoro/test-samples-impl.sh ]; then + cp "${TMPDIR}/test-samples-impl.sh" .kokoro/test-samples-impl.sh fi +fi - if [[ $EXIT -ne 0 ]]; then - RTN=1 - echo -e "\n Testing failed: Nox returned a non-zero exit code. \n" - else - echo -e "\n Testing completed.\n" - fi - -done -cd "$ROOT" - -# Workaround for Kokoro permissions issue: delete secrets -rm testing/{test-env.sh,client-secrets.json,service-account.json} - -exit "$RTN" +exec .kokoro/test-samples-impl.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9024b1..32302e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,6 @@ repos: hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.0 hooks: - id: flake8 diff --git a/README.rst b/README.rst index 81d78f1..d63a58d 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,5 @@ -Python Client for Cloud Billing API -============================================== - -|ga| |pypi| |versions| - -`Cloud Billing API`_: Allows developers to manage billing for their Google Cloud Platform -projects programmatically. - -- `Client Library Documentation`_ -- `Product Documentation`_ - -.. |ga| image:: https://img.shields.io/badge/support-GA-gold.svg - :target: https://github.com/googleapis/google-cloud-python/blob/master/README.rst#general-availability -.. |pypi| image:: https://img.shields.io/pypi/v/google-cloud-billing.svg - :target: https://pypi.org/project/google-cloud-billing/ -.. |versions| image:: https://img.shields.io/pypi/pyversions/google-cloud-billing.svg - :target: https://pypi.org/project/google-cloud-billing/ -.. _Cloud Billing API: https://cloud.google.com/billing -.. _Client Library Documentation: https://googleapis.dev/python/cloudbilling/latest -.. _Product Documentation: https://cloud.google.com/billing +Python Client for Google Cloud Billing API +================================================= Quick Start ----------- @@ -26,13 +8,12 @@ In order to use this library, you first need to go through the following steps: 1. `Select or create a Cloud Platform project.`_ 2. `Enable billing for your project.`_ -3. `Enable the Cloud Billing API.`_ +3. Enable the Google Cloud Billing API. 4. `Setup Authentication.`_ .. _Select or create a Cloud Platform project.: https://console.cloud.google.com/project .. _Enable billing for your project.: https://cloud.google.com/billing/docs/how-to/modify-project#enable_billing_for_a_project -.. _Enable the Cloud Billing API.: https://cloud.google.com/billing -.. _Setup Authentication.: https://googleapis.github.io/google-cloud-python/latest/core/auth.html +.. _Setup Authentication.: https://googleapis.dev/python/google-api-core/latest/auth.html Installation ~~~~~~~~~~~~ @@ -53,10 +34,9 @@ Mac/Linux .. code-block:: console - pip install virtualenv - virtualenv + python3 -m venv source /bin/activate - /bin/pip install google-cloud-billing + /bin/pip install /path/to/library Windows @@ -64,20 +44,6 @@ Windows .. code-block:: console - pip install virtualenv - virtualenv + python3 -m venv \Scripts\activate - \Scripts\pip.exe install google-cloud-billing - -Next Steps -~~~~~~~~~~ - -- Read the `Client Library Documentation`_ for Cloud Billing API - API to see other available methods on the client. -- Read the `Cloud Billing API Product documentation`_ to learn - more about the product and see How-to Guides. -- View this `README`_ to see the full list of Cloud - APIs that we cover. - -.. _Cloud Billing API Product documentation: https://cloud.google.com/billing -.. _README: https://github.com/googleapis/google-cloud-python/blob/master/README.rst + \Scripts\pip.exe install \path\to\library diff --git a/docs/billing_v1/cloud_billing.rst b/docs/billing_v1/cloud_billing.rst new file mode 100644 index 0000000..2a4ff02 --- /dev/null +++ b/docs/billing_v1/cloud_billing.rst @@ -0,0 +1,11 @@ +CloudBilling +------------------------------ + +.. automodule:: google.cloud.billing_v1.services.cloud_billing + :members: + :inherited-members: + + +.. automodule:: google.cloud.billing_v1.services.cloud_billing.pagers + :members: + :inherited-members: diff --git a/docs/billing_v1/cloud_catalog.rst b/docs/billing_v1/cloud_catalog.rst new file mode 100644 index 0000000..db04e68 --- /dev/null +++ b/docs/billing_v1/cloud_catalog.rst @@ -0,0 +1,11 @@ +CloudCatalog +------------------------------ + +.. automodule:: google.cloud.billing_v1.services.cloud_catalog + :members: + :inherited-members: + + +.. automodule:: google.cloud.billing_v1.services.cloud_catalog.pagers + :members: + :inherited-members: diff --git a/docs/billing_v1/services.rst b/docs/billing_v1/services.rst index b7511c4..9348836 100644 --- a/docs/billing_v1/services.rst +++ b/docs/billing_v1/services.rst @@ -1,9 +1,7 @@ Services for Google Cloud Billing v1 API ======================================== +.. toctree:: + :maxdepth: 2 -.. automodule:: google.cloud.billing_v1.services.cloud_billing - :members: - :inherited-members: -.. automodule:: google.cloud.billing_v1.services.cloud_catalog - :members: - :inherited-members: + cloud_billing + cloud_catalog diff --git a/docs/billing_v1/types.rst b/docs/billing_v1/types.rst index 75c1f9f..f51e922 100644 --- a/docs/billing_v1/types.rst +++ b/docs/billing_v1/types.rst @@ -3,4 +3,5 @@ Types for Google Cloud Billing v1 API .. automodule:: google.cloud.billing_v1.types :members: + :undoc-members: :show-inheritance: diff --git a/google/cloud/billing_v1/services/cloud_billing/async_client.py b/google/cloud/billing_v1/services/cloud_billing/async_client.py index ceab5c4..f56bbdf 100644 --- a/google/cloud/billing_v1/services/cloud_billing/async_client.py +++ b/google/cloud/billing_v1/services/cloud_billing/async_client.py @@ -73,7 +73,36 @@ class CloudBillingAsyncClient: CloudBillingClient.parse_common_location_path ) - from_service_account_file = CloudBillingClient.from_service_account_file + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CloudBillingAsyncClient: The constructed client. + """ + return CloudBillingClient.from_service_account_info.__func__(CloudBillingAsyncClient, info, *args, **kwargs) # type: ignore + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CloudBillingAsyncClient: The constructed client. + """ + return CloudBillingClient.from_service_account_file.__func__(CloudBillingAsyncClient, filename, *args, **kwargs) # type: ignore + from_service_account_json = from_service_account_file @property @@ -151,13 +180,14 @@ async def get_billing_account( account `__. Args: - request (:class:`~.cloud_billing.GetBillingAccountRequest`): + request (:class:`google.cloud.billing_v1.types.GetBillingAccountRequest`): The request object. Request message for `GetBillingAccount`. name (:class:`str`): Required. The resource name of the billing account to retrieve. For example, ``billingAccounts/012345-567890-ABCDEF``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -169,10 +199,10 @@ async def get_billing_account( sent along with the request as metadata. Returns: - ~.cloud_billing.BillingAccount: - A billing account in `GCP - Console `__. You can - assign a billing account to one or more projects. + google.cloud.billing_v1.types.BillingAccount: + A billing account in [GCP Console](\ https://console.cloud.google.com/). + You can assign a billing account to one or more + projects. """ # Create or coerce a protobuf request object. @@ -204,6 +234,7 @@ async def get_billing_account( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -234,7 +265,7 @@ async def list_billing_accounts( `view `__. Args: - request (:class:`~.cloud_billing.ListBillingAccountsRequest`): + request (:class:`google.cloud.billing_v1.types.ListBillingAccountsRequest`): The request object. Request message for `ListBillingAccounts`. @@ -245,8 +276,8 @@ async def list_billing_accounts( sent along with the request as metadata. Returns: - ~.pagers.ListBillingAccountsAsyncPager: - Response message for ``ListBillingAccounts``. + google.cloud.billing_v1.services.cloud_billing.pagers.ListBillingAccountsAsyncPager: + Response message for ListBillingAccounts. Iterating over this object will yield results and resolve additional pages automatically. @@ -267,6 +298,7 @@ async def list_billing_accounts( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -302,19 +334,21 @@ async def update_billing_account( of the billing account. Args: - request (:class:`~.cloud_billing.UpdateBillingAccountRequest`): + request (:class:`google.cloud.billing_v1.types.UpdateBillingAccountRequest`): The request object. Request message for `UpdateBillingAccount`. name (:class:`str`): Required. The name of the billing account resource to be updated. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. - account (:class:`~.cloud_billing.BillingAccount`): + account (:class:`google.cloud.billing_v1.types.BillingAccount`): Required. The billing account resource to replace the resource on the server. + This corresponds to the ``account`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -326,10 +360,10 @@ async def update_billing_account( sent along with the request as metadata. Returns: - ~.cloud_billing.BillingAccount: - A billing account in `GCP - Console `__. You can - assign a billing account to one or more projects. + google.cloud.billing_v1.types.BillingAccount: + A billing account in [GCP Console](\ https://console.cloud.google.com/). + You can assign a billing account to one or more + projects. """ # Create or coerce a protobuf request object. @@ -363,6 +397,7 @@ async def update_billing_account( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -401,16 +436,17 @@ async def create_billing_account( been provisioned as a reseller account. Args: - request (:class:`~.cloud_billing.CreateBillingAccountRequest`): + request (:class:`google.cloud.billing_v1.types.CreateBillingAccountRequest`): The request object. Request message for `CreateBillingAccount`. - billing_account (:class:`~.cloud_billing.BillingAccount`): + billing_account (:class:`google.cloud.billing_v1.types.BillingAccount`): Required. The billing account resource to create. Currently CreateBillingAccount only supports subaccount creation, so any created billing accounts must be under a provided master billing account. + This corresponds to the ``billing_account`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -422,10 +458,10 @@ async def create_billing_account( sent along with the request as metadata. Returns: - ~.cloud_billing.BillingAccount: - A billing account in `GCP - Console `__. You can - assign a billing account to one or more projects. + google.cloud.billing_v1.types.BillingAccount: + A billing account in [GCP Console](\ https://console.cloud.google.com/). + You can assign a billing account to one or more + projects. """ # Create or coerce a protobuf request object. @@ -476,13 +512,14 @@ async def list_project_billing_info( `viewers `__. Args: - request (:class:`~.cloud_billing.ListProjectBillingInfoRequest`): + request (:class:`google.cloud.billing_v1.types.ListProjectBillingInfoRequest`): The request object. Request message for `ListProjectBillingInfo`. name (:class:`str`): Required. The resource name of the billing account associated with the projects that you want to list. For example, ``billingAccounts/012345-567890-ABCDEF``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -494,8 +531,8 @@ async def list_project_billing_info( sent along with the request as metadata. Returns: - ~.pagers.ListProjectBillingInfoAsyncPager: - Request message for ``ListProjectBillingInfoResponse``. + google.cloud.billing_v1.services.cloud_billing.pagers.ListProjectBillingInfoAsyncPager: + Request message for ListProjectBillingInfoResponse. Iterating over this object will yield results and resolve additional pages automatically. @@ -530,6 +567,7 @@ async def list_project_billing_info( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -567,13 +605,14 @@ async def get_project_billing_info( project `__. Args: - request (:class:`~.cloud_billing.GetProjectBillingInfoRequest`): + request (:class:`google.cloud.billing_v1.types.GetProjectBillingInfoRequest`): The request object. Request message for `GetProjectBillingInfo`. name (:class:`str`): Required. The resource name of the project for which billing information is retrieved. For example, ``projects/tokyo-rain-123``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -585,7 +624,7 @@ async def get_project_billing_info( sent along with the request as metadata. Returns: - ~.cloud_billing.ProjectBillingInfo: + google.cloud.billing_v1.types.ProjectBillingInfo: Encapsulation of billing information for a GCP Console project. A project has at most one associated billing account @@ -622,6 +661,7 @@ async def get_project_billing_info( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -687,20 +727,22 @@ async def update_project_billing_info( account. Args: - request (:class:`~.cloud_billing.UpdateProjectBillingInfoRequest`): + request (:class:`google.cloud.billing_v1.types.UpdateProjectBillingInfoRequest`): The request object. Request message for `UpdateProjectBillingInfo`. name (:class:`str`): Required. The resource name of the project associated with the billing information that you want to update. For example, ``projects/tokyo-rain-123``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. - project_billing_info (:class:`~.cloud_billing.ProjectBillingInfo`): + project_billing_info (:class:`google.cloud.billing_v1.types.ProjectBillingInfo`): The new billing information for the project. Read-only fields are ignored; thus, you can leave empty all fields except ``billing_account_name``. + This corresponds to the ``project_billing_info`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -712,7 +754,7 @@ async def update_project_billing_info( sent along with the request as metadata. Returns: - ~.cloud_billing.ProjectBillingInfo: + google.cloud.billing_v1.types.ProjectBillingInfo: Encapsulation of billing information for a GCP Console project. A project has at most one associated billing account @@ -751,6 +793,7 @@ async def update_project_billing_info( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -783,7 +826,7 @@ async def get_iam_policy( `viewers `__. Args: - request (:class:`~.iam_policy.GetIamPolicyRequest`): + request (:class:`google.iam.v1.iam_policy_pb2.GetIamPolicyRequest`): The request object. Request message for `GetIamPolicy` method. resource (:class:`str`): @@ -791,6 +834,7 @@ async def get_iam_policy( policy is being requested. See the operation documentation for the appropriate value for this field. + This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -802,72 +846,62 @@ async def get_iam_policy( sent along with the request as metadata. Returns: - ~.policy.Policy: - Defines an Identity and Access Management (IAM) policy. - It is used to specify access control policies for Cloud - Platform resources. - - A ``Policy`` is a collection of ``bindings``. A - ``binding`` binds one or more ``members`` to a single - ``role``. Members can be user accounts, service - accounts, Google groups, and domains (such as G Suite). - A ``role`` is a named list of permissions (defined by - IAM or configured by users). A ``binding`` can - optionally specify a ``condition``, which is a logic - expression that further constrains the role binding - based on attributes about the request and/or target - resource. - - **JSON Example** - - :: - - { - "bindings": [ - { - "role": "roles/resourcemanager.organizationAdmin", - "members": [ - "user:mike@example.com", - "group:admins@example.com", - "domain:google.com", - "serviceAccount:my-project-id@appspot.gserviceaccount.com" - ] - }, - { - "role": "roles/resourcemanager.organizationViewer", - "members": ["user:eve@example.com"], - "condition": { - "title": "expirable access", - "description": "Does not grant access after Sep 2020", - "expression": "request.time < - timestamp('2020-10-01T00:00:00.000Z')", - } - } - ] - } - - **YAML Example** - - :: - - bindings: - - members: - - user:mike@example.com - - group:admins@example.com - - domain:google.com - - serviceAccount:my-project-id@appspot.gserviceaccount.com - role: roles/resourcemanager.organizationAdmin - - members: - - user:eve@example.com - role: roles/resourcemanager.organizationViewer - condition: - title: expirable access - description: Does not grant access after Sep 2020 - expression: request.time < timestamp('2020-10-01T00:00:00.000Z') - - For a description of IAM and its features, see the `IAM - developer's - guide `__. + google.iam.v1.policy_pb2.Policy: + Defines an Identity and Access Management (IAM) policy. It is used to + specify access control policies for Cloud Platform + resources. + + A Policy is a collection of bindings. A binding binds + one or more members to a single role. Members can be + user accounts, service accounts, Google groups, and + domains (such as G Suite). A role is a named list of + permissions (defined by IAM or configured by users). + A binding can optionally specify a condition, which + is a logic expression that further constrains the + role binding based on attributes about the request + and/or target resource. + + **JSON Example** + + { + "bindings": [ + { + "role": + "roles/resourcemanager.organizationAdmin", + "members": [ "user:mike@example.com", + "group:admins@example.com", + "domain:google.com", + "serviceAccount:my-project-id@appspot.gserviceaccount.com" + ] + + }, { "role": + "roles/resourcemanager.organizationViewer", + "members": ["user:eve@example.com"], + "condition": { "title": "expirable access", + "description": "Does not grant access after + Sep 2020", "expression": "request.time < + timestamp('2020-10-01T00:00:00.000Z')", } } + + ] + + } + + **YAML Example** + + bindings: - members: - user:\ mike@example.com - + group:\ admins@example.com - domain:google.com - + serviceAccount:\ my-project-id@appspot.gserviceaccount.com + role: roles/resourcemanager.organizationAdmin - + members: - user:\ eve@example.com role: + roles/resourcemanager.organizationViewer + condition: title: expirable access description: + Does not grant access after Sep 2020 expression: + request.time < + timestamp('2020-10-01T00:00:00.000Z') + + For a description of IAM and its features, see the + [IAM developer's + guide](\ https://cloud.google.com/iam/docs). """ # Create or coerce a protobuf request object. @@ -899,6 +933,7 @@ async def get_iam_policy( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -932,7 +967,7 @@ async def set_iam_policy( `administrators `__. Args: - request (:class:`~.iam_policy.SetIamPolicyRequest`): + request (:class:`google.iam.v1.iam_policy_pb2.SetIamPolicyRequest`): The request object. Request message for `SetIamPolicy` method. resource (:class:`str`): @@ -940,6 +975,7 @@ async def set_iam_policy( policy is being specified. See the operation documentation for the appropriate value for this field. + This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -951,72 +987,62 @@ async def set_iam_policy( sent along with the request as metadata. Returns: - ~.policy.Policy: - Defines an Identity and Access Management (IAM) policy. - It is used to specify access control policies for Cloud - Platform resources. - - A ``Policy`` is a collection of ``bindings``. A - ``binding`` binds one or more ``members`` to a single - ``role``. Members can be user accounts, service - accounts, Google groups, and domains (such as G Suite). - A ``role`` is a named list of permissions (defined by - IAM or configured by users). A ``binding`` can - optionally specify a ``condition``, which is a logic - expression that further constrains the role binding - based on attributes about the request and/or target - resource. - - **JSON Example** - - :: - - { - "bindings": [ - { - "role": "roles/resourcemanager.organizationAdmin", - "members": [ - "user:mike@example.com", - "group:admins@example.com", - "domain:google.com", - "serviceAccount:my-project-id@appspot.gserviceaccount.com" - ] - }, - { - "role": "roles/resourcemanager.organizationViewer", - "members": ["user:eve@example.com"], - "condition": { - "title": "expirable access", - "description": "Does not grant access after Sep 2020", - "expression": "request.time < - timestamp('2020-10-01T00:00:00.000Z')", - } - } - ] - } - - **YAML Example** - - :: - - bindings: - - members: - - user:mike@example.com - - group:admins@example.com - - domain:google.com - - serviceAccount:my-project-id@appspot.gserviceaccount.com - role: roles/resourcemanager.organizationAdmin - - members: - - user:eve@example.com - role: roles/resourcemanager.organizationViewer - condition: - title: expirable access - description: Does not grant access after Sep 2020 - expression: request.time < timestamp('2020-10-01T00:00:00.000Z') - - For a description of IAM and its features, see the `IAM - developer's - guide `__. + google.iam.v1.policy_pb2.Policy: + Defines an Identity and Access Management (IAM) policy. It is used to + specify access control policies for Cloud Platform + resources. + + A Policy is a collection of bindings. A binding binds + one or more members to a single role. Members can be + user accounts, service accounts, Google groups, and + domains (such as G Suite). A role is a named list of + permissions (defined by IAM or configured by users). + A binding can optionally specify a condition, which + is a logic expression that further constrains the + role binding based on attributes about the request + and/or target resource. + + **JSON Example** + + { + "bindings": [ + { + "role": + "roles/resourcemanager.organizationAdmin", + "members": [ "user:mike@example.com", + "group:admins@example.com", + "domain:google.com", + "serviceAccount:my-project-id@appspot.gserviceaccount.com" + ] + + }, { "role": + "roles/resourcemanager.organizationViewer", + "members": ["user:eve@example.com"], + "condition": { "title": "expirable access", + "description": "Does not grant access after + Sep 2020", "expression": "request.time < + timestamp('2020-10-01T00:00:00.000Z')", } } + + ] + + } + + **YAML Example** + + bindings: - members: - user:\ mike@example.com - + group:\ admins@example.com - domain:google.com - + serviceAccount:\ my-project-id@appspot.gserviceaccount.com + role: roles/resourcemanager.organizationAdmin - + members: - user:\ eve@example.com role: + roles/resourcemanager.organizationViewer + condition: title: expirable access description: + Does not grant access after Sep 2020 expression: + request.time < + timestamp('2020-10-01T00:00:00.000Z') + + For a description of IAM and its features, see the + [IAM developer's + guide](\ https://cloud.google.com/iam/docs). """ # Create or coerce a protobuf request object. @@ -1048,6 +1074,7 @@ async def set_iam_policy( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, @@ -1082,7 +1109,7 @@ async def test_iam_permissions( resource. Args: - request (:class:`~.iam_policy.TestIamPermissionsRequest`): + request (:class:`google.iam.v1.iam_policy_pb2.TestIamPermissionsRequest`): The request object. Request message for `TestIamPermissions` method. resource (:class:`str`): @@ -1090,6 +1117,7 @@ async def test_iam_permissions( policy detail is being requested. See the operation documentation for the appropriate value for this field. + This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -1098,6 +1126,7 @@ async def test_iam_permissions( Permissions with wildcards (such as '*' or 'storage.*') are not allowed. For more information see `IAM Overview `__. + This corresponds to the ``permissions`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -1109,8 +1138,8 @@ async def test_iam_permissions( sent along with the request as metadata. Returns: - ~.iam_policy.TestIamPermissionsResponse: - Response message for ``TestIamPermissions`` method. + google.iam.v1.iam_policy_pb2.TestIamPermissionsResponse: + Response message for TestIamPermissions method. """ # Create or coerce a protobuf request object. # Sanity check: If we got a request object, we should *not* have @@ -1143,6 +1172,7 @@ async def test_iam_permissions( predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=DEFAULT_CLIENT_INFO, diff --git a/google/cloud/billing_v1/services/cloud_billing/client.py b/google/cloud/billing_v1/services/cloud_billing/client.py index 1db4189..4c5b232 100644 --- a/google/cloud/billing_v1/services/cloud_billing/client.py +++ b/google/cloud/billing_v1/services/cloud_billing/client.py @@ -112,6 +112,22 @@ def _get_default_mtls_endpoint(api_endpoint): DEFAULT_ENDPOINT ) + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CloudBillingClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + @classmethod def from_service_account_file(cls, filename: str, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -124,7 +140,7 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): kwargs: Additional arguments to pass to the constructor. Returns: - {@api.name}: The constructed client. + CloudBillingClient: The constructed client. """ credentials = service_account.Credentials.from_service_account_file(filename) kwargs["credentials"] = credentials @@ -216,10 +232,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - transport (Union[str, ~.CloudBillingTransport]): The + transport (Union[str, CloudBillingTransport]): The transport to use. If set to None, a transport is chosen automatically. - client_options (client_options_lib.ClientOptions): Custom options for the + client_options (google.api_core.client_options.ClientOptions): Custom options for the client. It won't take effect if a ``transport`` instance is provided. (1) The ``api_endpoint`` property can be used to override the default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT @@ -255,21 +271,17 @@ def __init__( util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) ) - ssl_credentials = None + client_cert_source_func = None is_mtls = False if use_client_cert: if client_options.client_cert_source: - import grpc # type: ignore - - cert, key = client_options.client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) is_mtls = True + client_cert_source_func = client_options.client_cert_source else: - creds = SslCredentials() - is_mtls = creds.is_mtls - ssl_credentials = creds.ssl_credentials if is_mtls else None + is_mtls = mtls.has_default_client_cert_source() + client_cert_source_func = ( + mtls.default_client_cert_source() if is_mtls else None + ) # Figure out which api endpoint to use. if client_options.api_endpoint is not None: @@ -312,7 +324,7 @@ def __init__( credentials_file=client_options.credentials_file, host=api_endpoint, scopes=client_options.scopes, - ssl_channel_credentials=ssl_credentials, + client_cert_source_for_mtls=client_cert_source_func, quota_project_id=client_options.quota_project_id, client_info=client_info, ) @@ -331,13 +343,14 @@ def get_billing_account( account `__. Args: - request (:class:`~.cloud_billing.GetBillingAccountRequest`): + request (google.cloud.billing_v1.types.GetBillingAccountRequest): The request object. Request message for `GetBillingAccount`. - name (:class:`str`): + name (str): Required. The resource name of the billing account to retrieve. For example, ``billingAccounts/012345-567890-ABCDEF``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -349,10 +362,10 @@ def get_billing_account( sent along with the request as metadata. Returns: - ~.cloud_billing.BillingAccount: - A billing account in `GCP - Console `__. You can - assign a billing account to one or more projects. + google.cloud.billing_v1.types.BillingAccount: + A billing account in [GCP Console](\ https://console.cloud.google.com/). + You can assign a billing account to one or more + projects. """ # Create or coerce a protobuf request object. @@ -407,7 +420,7 @@ def list_billing_accounts( `view `__. Args: - request (:class:`~.cloud_billing.ListBillingAccountsRequest`): + request (google.cloud.billing_v1.types.ListBillingAccountsRequest): The request object. Request message for `ListBillingAccounts`. @@ -418,8 +431,8 @@ def list_billing_accounts( sent along with the request as metadata. Returns: - ~.pagers.ListBillingAccountsPager: - Response message for ``ListBillingAccounts``. + google.cloud.billing_v1.services.cloud_billing.pagers.ListBillingAccountsPager: + Response message for ListBillingAccounts. Iterating over this object will yield results and resolve additional pages automatically. @@ -468,19 +481,21 @@ def update_billing_account( of the billing account. Args: - request (:class:`~.cloud_billing.UpdateBillingAccountRequest`): + request (google.cloud.billing_v1.types.UpdateBillingAccountRequest): The request object. Request message for `UpdateBillingAccount`. - name (:class:`str`): + name (str): Required. The name of the billing account resource to be updated. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. - account (:class:`~.cloud_billing.BillingAccount`): + account (google.cloud.billing_v1.types.BillingAccount): Required. The billing account resource to replace the resource on the server. + This corresponds to the ``account`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -492,10 +507,10 @@ def update_billing_account( sent along with the request as metadata. Returns: - ~.cloud_billing.BillingAccount: - A billing account in `GCP - Console `__. You can - assign a billing account to one or more projects. + google.cloud.billing_v1.types.BillingAccount: + A billing account in [GCP Console](\ https://console.cloud.google.com/). + You can assign a billing account to one or more + projects. """ # Create or coerce a protobuf request object. @@ -560,16 +575,17 @@ def create_billing_account( been provisioned as a reseller account. Args: - request (:class:`~.cloud_billing.CreateBillingAccountRequest`): + request (google.cloud.billing_v1.types.CreateBillingAccountRequest): The request object. Request message for `CreateBillingAccount`. - billing_account (:class:`~.cloud_billing.BillingAccount`): + billing_account (google.cloud.billing_v1.types.BillingAccount): Required. The billing account resource to create. Currently CreateBillingAccount only supports subaccount creation, so any created billing accounts must be under a provided master billing account. + This corresponds to the ``billing_account`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -581,10 +597,10 @@ def create_billing_account( sent along with the request as metadata. Returns: - ~.cloud_billing.BillingAccount: - A billing account in `GCP - Console `__. You can - assign a billing account to one or more projects. + google.cloud.billing_v1.types.BillingAccount: + A billing account in [GCP Console](\ https://console.cloud.google.com/). + You can assign a billing account to one or more + projects. """ # Create or coerce a protobuf request object. @@ -636,13 +652,14 @@ def list_project_billing_info( `viewers `__. Args: - request (:class:`~.cloud_billing.ListProjectBillingInfoRequest`): + request (google.cloud.billing_v1.types.ListProjectBillingInfoRequest): The request object. Request message for `ListProjectBillingInfo`. - name (:class:`str`): + name (str): Required. The resource name of the billing account associated with the projects that you want to list. For example, ``billingAccounts/012345-567890-ABCDEF``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -654,8 +671,8 @@ def list_project_billing_info( sent along with the request as metadata. Returns: - ~.pagers.ListProjectBillingInfoPager: - Request message for ``ListProjectBillingInfoResponse``. + google.cloud.billing_v1.services.cloud_billing.pagers.ListProjectBillingInfoPager: + Request message for ListProjectBillingInfoResponse. Iterating over this object will yield results and resolve additional pages automatically. @@ -722,13 +739,14 @@ def get_project_billing_info( project `__. Args: - request (:class:`~.cloud_billing.GetProjectBillingInfoRequest`): + request (google.cloud.billing_v1.types.GetProjectBillingInfoRequest): The request object. Request message for `GetProjectBillingInfo`. - name (:class:`str`): + name (str): Required. The resource name of the project for which billing information is retrieved. For example, ``projects/tokyo-rain-123``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -740,7 +758,7 @@ def get_project_billing_info( sent along with the request as metadata. Returns: - ~.cloud_billing.ProjectBillingInfo: + google.cloud.billing_v1.types.ProjectBillingInfo: Encapsulation of billing information for a GCP Console project. A project has at most one associated billing account @@ -835,20 +853,22 @@ def update_project_billing_info( account. Args: - request (:class:`~.cloud_billing.UpdateProjectBillingInfoRequest`): + request (google.cloud.billing_v1.types.UpdateProjectBillingInfoRequest): The request object. Request message for `UpdateProjectBillingInfo`. - name (:class:`str`): + name (str): Required. The resource name of the project associated with the billing information that you want to update. For example, ``projects/tokyo-rain-123``. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. - project_billing_info (:class:`~.cloud_billing.ProjectBillingInfo`): + project_billing_info (google.cloud.billing_v1.types.ProjectBillingInfo): The new billing information for the project. Read-only fields are ignored; thus, you can leave empty all fields except ``billing_account_name``. + This corresponds to the ``project_billing_info`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -860,7 +880,7 @@ def update_project_billing_info( sent along with the request as metadata. Returns: - ~.cloud_billing.ProjectBillingInfo: + google.cloud.billing_v1.types.ProjectBillingInfo: Encapsulation of billing information for a GCP Console project. A project has at most one associated billing account @@ -926,14 +946,15 @@ def get_iam_policy( `viewers `__. Args: - request (:class:`~.iam_policy.GetIamPolicyRequest`): + request (google.iam.v1.iam_policy_pb2.GetIamPolicyRequest): The request object. Request message for `GetIamPolicy` method. - resource (:class:`str`): + resource (str): REQUIRED: The resource for which the policy is being requested. See the operation documentation for the appropriate value for this field. + This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -945,72 +966,62 @@ def get_iam_policy( sent along with the request as metadata. Returns: - ~.policy.Policy: - Defines an Identity and Access Management (IAM) policy. - It is used to specify access control policies for Cloud - Platform resources. - - A ``Policy`` is a collection of ``bindings``. A - ``binding`` binds one or more ``members`` to a single - ``role``. Members can be user accounts, service - accounts, Google groups, and domains (such as G Suite). - A ``role`` is a named list of permissions (defined by - IAM or configured by users). A ``binding`` can - optionally specify a ``condition``, which is a logic - expression that further constrains the role binding - based on attributes about the request and/or target - resource. - - **JSON Example** - - :: - - { - "bindings": [ - { - "role": "roles/resourcemanager.organizationAdmin", - "members": [ - "user:mike@example.com", - "group:admins@example.com", - "domain:google.com", - "serviceAccount:my-project-id@appspot.gserviceaccount.com" - ] - }, - { - "role": "roles/resourcemanager.organizationViewer", - "members": ["user:eve@example.com"], - "condition": { - "title": "expirable access", - "description": "Does not grant access after Sep 2020", - "expression": "request.time < - timestamp('2020-10-01T00:00:00.000Z')", - } - } - ] - } - - **YAML Example** - - :: - - bindings: - - members: - - user:mike@example.com - - group:admins@example.com - - domain:google.com - - serviceAccount:my-project-id@appspot.gserviceaccount.com - role: roles/resourcemanager.organizationAdmin - - members: - - user:eve@example.com - role: roles/resourcemanager.organizationViewer - condition: - title: expirable access - description: Does not grant access after Sep 2020 - expression: request.time < timestamp('2020-10-01T00:00:00.000Z') - - For a description of IAM and its features, see the `IAM - developer's - guide `__. + google.iam.v1.policy_pb2.Policy: + Defines an Identity and Access Management (IAM) policy. It is used to + specify access control policies for Cloud Platform + resources. + + A Policy is a collection of bindings. A binding binds + one or more members to a single role. Members can be + user accounts, service accounts, Google groups, and + domains (such as G Suite). A role is a named list of + permissions (defined by IAM or configured by users). + A binding can optionally specify a condition, which + is a logic expression that further constrains the + role binding based on attributes about the request + and/or target resource. + + **JSON Example** + + { + "bindings": [ + { + "role": + "roles/resourcemanager.organizationAdmin", + "members": [ "user:mike@example.com", + "group:admins@example.com", + "domain:google.com", + "serviceAccount:my-project-id@appspot.gserviceaccount.com" + ] + + }, { "role": + "roles/resourcemanager.organizationViewer", + "members": ["user:eve@example.com"], + "condition": { "title": "expirable access", + "description": "Does not grant access after + Sep 2020", "expression": "request.time < + timestamp('2020-10-01T00:00:00.000Z')", } } + + ] + + } + + **YAML Example** + + bindings: - members: - user:\ mike@example.com - + group:\ admins@example.com - domain:google.com - + serviceAccount:\ my-project-id@appspot.gserviceaccount.com + role: roles/resourcemanager.organizationAdmin - + members: - user:\ eve@example.com role: + roles/resourcemanager.organizationViewer + condition: title: expirable access description: + Does not grant access after Sep 2020 expression: + request.time < + timestamp('2020-10-01T00:00:00.000Z') + + For a description of IAM and its features, see the + [IAM developer's + guide](\ https://cloud.google.com/iam/docs). """ # Create or coerce a protobuf request object. @@ -1023,13 +1034,16 @@ def get_iam_policy( "the individual field arguments should be set." ) - # The request isn't a proto-plus wrapped type, - # so it must be constructed via keyword expansion. if isinstance(request, dict): + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. request = iam_policy.GetIamPolicyRequest(**request) - elif not request: - request = iam_policy.GetIamPolicyRequest(resource=resource,) + # Null request, just make one. + request = iam_policy.GetIamPolicyRequest() + + if resource is not None: + request.resource = resource # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. @@ -1063,14 +1077,15 @@ def set_iam_policy( `administrators `__. Args: - request (:class:`~.iam_policy.SetIamPolicyRequest`): + request (google.iam.v1.iam_policy_pb2.SetIamPolicyRequest): The request object. Request message for `SetIamPolicy` method. - resource (:class:`str`): + resource (str): REQUIRED: The resource for which the policy is being specified. See the operation documentation for the appropriate value for this field. + This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -1082,72 +1097,62 @@ def set_iam_policy( sent along with the request as metadata. Returns: - ~.policy.Policy: - Defines an Identity and Access Management (IAM) policy. - It is used to specify access control policies for Cloud - Platform resources. - - A ``Policy`` is a collection of ``bindings``. A - ``binding`` binds one or more ``members`` to a single - ``role``. Members can be user accounts, service - accounts, Google groups, and domains (such as G Suite). - A ``role`` is a named list of permissions (defined by - IAM or configured by users). A ``binding`` can - optionally specify a ``condition``, which is a logic - expression that further constrains the role binding - based on attributes about the request and/or target - resource. - - **JSON Example** - - :: - - { - "bindings": [ - { - "role": "roles/resourcemanager.organizationAdmin", - "members": [ - "user:mike@example.com", - "group:admins@example.com", - "domain:google.com", - "serviceAccount:my-project-id@appspot.gserviceaccount.com" - ] - }, - { - "role": "roles/resourcemanager.organizationViewer", - "members": ["user:eve@example.com"], - "condition": { - "title": "expirable access", - "description": "Does not grant access after Sep 2020", - "expression": "request.time < - timestamp('2020-10-01T00:00:00.000Z')", - } - } - ] - } - - **YAML Example** - - :: - - bindings: - - members: - - user:mike@example.com - - group:admins@example.com - - domain:google.com - - serviceAccount:my-project-id@appspot.gserviceaccount.com - role: roles/resourcemanager.organizationAdmin - - members: - - user:eve@example.com - role: roles/resourcemanager.organizationViewer - condition: - title: expirable access - description: Does not grant access after Sep 2020 - expression: request.time < timestamp('2020-10-01T00:00:00.000Z') - - For a description of IAM and its features, see the `IAM - developer's - guide `__. + google.iam.v1.policy_pb2.Policy: + Defines an Identity and Access Management (IAM) policy. It is used to + specify access control policies for Cloud Platform + resources. + + A Policy is a collection of bindings. A binding binds + one or more members to a single role. Members can be + user accounts, service accounts, Google groups, and + domains (such as G Suite). A role is a named list of + permissions (defined by IAM or configured by users). + A binding can optionally specify a condition, which + is a logic expression that further constrains the + role binding based on attributes about the request + and/or target resource. + + **JSON Example** + + { + "bindings": [ + { + "role": + "roles/resourcemanager.organizationAdmin", + "members": [ "user:mike@example.com", + "group:admins@example.com", + "domain:google.com", + "serviceAccount:my-project-id@appspot.gserviceaccount.com" + ] + + }, { "role": + "roles/resourcemanager.organizationViewer", + "members": ["user:eve@example.com"], + "condition": { "title": "expirable access", + "description": "Does not grant access after + Sep 2020", "expression": "request.time < + timestamp('2020-10-01T00:00:00.000Z')", } } + + ] + + } + + **YAML Example** + + bindings: - members: - user:\ mike@example.com - + group:\ admins@example.com - domain:google.com - + serviceAccount:\ my-project-id@appspot.gserviceaccount.com + role: roles/resourcemanager.organizationAdmin - + members: - user:\ eve@example.com role: + roles/resourcemanager.organizationViewer + condition: title: expirable access description: + Does not grant access after Sep 2020 expression: + request.time < + timestamp('2020-10-01T00:00:00.000Z') + + For a description of IAM and its features, see the + [IAM developer's + guide](\ https://cloud.google.com/iam/docs). """ # Create or coerce a protobuf request object. @@ -1160,13 +1165,16 @@ def set_iam_policy( "the individual field arguments should be set." ) - # The request isn't a proto-plus wrapped type, - # so it must be constructed via keyword expansion. if isinstance(request, dict): + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. request = iam_policy.SetIamPolicyRequest(**request) - elif not request: - request = iam_policy.SetIamPolicyRequest(resource=resource,) + # Null request, just make one. + request = iam_policy.SetIamPolicyRequest() + + if resource is not None: + request.resource = resource # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. @@ -1201,22 +1209,24 @@ def test_iam_permissions( resource. Args: - request (:class:`~.iam_policy.TestIamPermissionsRequest`): + request (google.iam.v1.iam_policy_pb2.TestIamPermissionsRequest): The request object. Request message for `TestIamPermissions` method. - resource (:class:`str`): + resource (str): REQUIRED: The resource for which the policy detail is being requested. See the operation documentation for the appropriate value for this field. + This corresponds to the ``resource`` field on the ``request`` instance; if ``request`` is provided, this should not be set. - permissions (:class:`Sequence[str]`): + permissions (Sequence[str]): The set of permissions to check for the ``resource``. Permissions with wildcards (such as '*' or 'storage.*') are not allowed. For more information see `IAM Overview `__. + This corresponds to the ``permissions`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -1228,8 +1238,8 @@ def test_iam_permissions( sent along with the request as metadata. Returns: - ~.iam_policy.TestIamPermissionsResponse: - Response message for ``TestIamPermissions`` method. + google.iam.v1.iam_policy_pb2.TestIamPermissionsResponse: + Response message for TestIamPermissions method. """ # Create or coerce a protobuf request object. # Sanity check: If we got a request object, we should *not* have @@ -1241,15 +1251,19 @@ def test_iam_permissions( "the individual field arguments should be set." ) - # The request isn't a proto-plus wrapped type, - # so it must be constructed via keyword expansion. if isinstance(request, dict): + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. request = iam_policy.TestIamPermissionsRequest(**request) - elif not request: - request = iam_policy.TestIamPermissionsRequest( - resource=resource, permissions=permissions, - ) + # Null request, just make one. + request = iam_policy.TestIamPermissionsRequest() + + if resource is not None: + request.resource = resource + + if permissions: + request.permissions.extend(permissions) # Wrap the RPC method; this adds retry and timeout information, # and friendly error handling. diff --git a/google/cloud/billing_v1/services/cloud_billing/pagers.py b/google/cloud/billing_v1/services/cloud_billing/pagers.py index d202714..a0fbaba 100644 --- a/google/cloud/billing_v1/services/cloud_billing/pagers.py +++ b/google/cloud/billing_v1/services/cloud_billing/pagers.py @@ -15,7 +15,16 @@ # limitations under the License. # -from typing import Any, AsyncIterable, Awaitable, Callable, Iterable, Sequence, Tuple +from typing import ( + Any, + AsyncIterable, + Awaitable, + Callable, + Iterable, + Sequence, + Tuple, + Optional, +) from google.cloud.billing_v1.types import cloud_billing @@ -24,7 +33,7 @@ class ListBillingAccountsPager: """A pager for iterating through ``list_billing_accounts`` requests. This class thinly wraps an initial - :class:`~.cloud_billing.ListBillingAccountsResponse` object, and + :class:`google.cloud.billing_v1.types.ListBillingAccountsResponse` object, and provides an ``__iter__`` method to iterate through its ``billing_accounts`` field. @@ -33,7 +42,7 @@ class ListBillingAccountsPager: through the ``billing_accounts`` field on the corresponding responses. - All the usual :class:`~.cloud_billing.ListBillingAccountsResponse` + All the usual :class:`google.cloud.billing_v1.types.ListBillingAccountsResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -51,9 +60,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_billing.ListBillingAccountsRequest`): + request (google.cloud.billing_v1.types.ListBillingAccountsRequest): The initial request object. - response (:class:`~.cloud_billing.ListBillingAccountsResponse`): + response (google.cloud.billing_v1.types.ListBillingAccountsResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. @@ -86,7 +95,7 @@ class ListBillingAccountsAsyncPager: """A pager for iterating through ``list_billing_accounts`` requests. This class thinly wraps an initial - :class:`~.cloud_billing.ListBillingAccountsResponse` object, and + :class:`google.cloud.billing_v1.types.ListBillingAccountsResponse` object, and provides an ``__aiter__`` method to iterate through its ``billing_accounts`` field. @@ -95,7 +104,7 @@ class ListBillingAccountsAsyncPager: through the ``billing_accounts`` field on the corresponding responses. - All the usual :class:`~.cloud_billing.ListBillingAccountsResponse` + All the usual :class:`google.cloud.billing_v1.types.ListBillingAccountsResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -113,9 +122,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_billing.ListBillingAccountsRequest`): + request (google.cloud.billing_v1.types.ListBillingAccountsRequest): The initial request object. - response (:class:`~.cloud_billing.ListBillingAccountsResponse`): + response (google.cloud.billing_v1.types.ListBillingAccountsResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. @@ -152,7 +161,7 @@ class ListProjectBillingInfoPager: """A pager for iterating through ``list_project_billing_info`` requests. This class thinly wraps an initial - :class:`~.cloud_billing.ListProjectBillingInfoResponse` object, and + :class:`google.cloud.billing_v1.types.ListProjectBillingInfoResponse` object, and provides an ``__iter__`` method to iterate through its ``project_billing_info`` field. @@ -161,7 +170,7 @@ class ListProjectBillingInfoPager: through the ``project_billing_info`` field on the corresponding responses. - All the usual :class:`~.cloud_billing.ListProjectBillingInfoResponse` + All the usual :class:`google.cloud.billing_v1.types.ListProjectBillingInfoResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -179,9 +188,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_billing.ListProjectBillingInfoRequest`): + request (google.cloud.billing_v1.types.ListProjectBillingInfoRequest): The initial request object. - response (:class:`~.cloud_billing.ListProjectBillingInfoResponse`): + response (google.cloud.billing_v1.types.ListProjectBillingInfoResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. @@ -214,7 +223,7 @@ class ListProjectBillingInfoAsyncPager: """A pager for iterating through ``list_project_billing_info`` requests. This class thinly wraps an initial - :class:`~.cloud_billing.ListProjectBillingInfoResponse` object, and + :class:`google.cloud.billing_v1.types.ListProjectBillingInfoResponse` object, and provides an ``__aiter__`` method to iterate through its ``project_billing_info`` field. @@ -223,7 +232,7 @@ class ListProjectBillingInfoAsyncPager: through the ``project_billing_info`` field on the corresponding responses. - All the usual :class:`~.cloud_billing.ListProjectBillingInfoResponse` + All the usual :class:`google.cloud.billing_v1.types.ListProjectBillingInfoResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -241,9 +250,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_billing.ListProjectBillingInfoRequest`): + request (google.cloud.billing_v1.types.ListProjectBillingInfoRequest): The initial request object. - response (:class:`~.cloud_billing.ListProjectBillingInfoResponse`): + response (google.cloud.billing_v1.types.ListProjectBillingInfoResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. diff --git a/google/cloud/billing_v1/services/cloud_billing/transports/base.py b/google/cloud/billing_v1/services/cloud_billing/transports/base.py index eaf061f..a59b9b4 100644 --- a/google/cloud/billing_v1/services/cloud_billing/transports/base.py +++ b/google/cloud/billing_v1/services/cloud_billing/transports/base.py @@ -69,10 +69,10 @@ def __init__( scope (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you're developing + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing your own client library. """ # Save the hostname. Default to port 443 (HTTPS) if none is specified. @@ -80,6 +80,9 @@ def __init__( host += ":443" self._host = host + # Save the scopes. + self._scopes = scopes or self.AUTH_SCOPES + # If no credentials are provided, then determine the appropriate # defaults. if credentials and credentials_file: @@ -89,20 +92,17 @@ def __init__( if credentials_file is not None: credentials, _ = auth.load_credentials_from_file( - credentials_file, scopes=scopes, quota_project_id=quota_project_id + credentials_file, scopes=self._scopes, quota_project_id=quota_project_id ) elif credentials is None: credentials, _ = auth.default( - scopes=scopes, quota_project_id=quota_project_id + scopes=self._scopes, quota_project_id=quota_project_id ) # Save the credentials. self._credentials = credentials - # Lifted into its own function so it can be stubbed out during tests. - self._prep_wrapped_messages(client_info) - def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. self._wrapped_methods = { @@ -115,6 +115,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -128,6 +129,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -141,6 +143,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -159,6 +162,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -172,6 +176,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -185,6 +190,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -198,6 +204,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -211,6 +218,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, @@ -224,6 +232,7 @@ def _prep_wrapped_messages(self, client_info): predicate=retries.if_exception_type( exceptions.DeadlineExceeded, exceptions.ServiceUnavailable, ), + deadline=60.0, ), default_timeout=60.0, client_info=client_info, diff --git a/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py b/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py index 3be41ab..94fb1ae 100644 --- a/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py +++ b/google/cloud/billing_v1/services/cloud_billing/transports/grpc.py @@ -60,6 +60,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id: Optional[str] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -90,6 +91,10 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. client_info (google.api_core.gapic_v1.client_info.ClientInfo): @@ -104,72 +109,60 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - warnings.warn( - "api_mtls_endpoint and client_cert_source are deprecated", - DeprecationWarning, - ) - host = ( - api_mtls_endpoint - if ":" in api_mtls_endpoint - else api_mtls_endpoint + ":443" - ) + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) - - # Create SSL credentials with client_cert_source or application - # default SSL credentials. - if client_cert_source: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) else: - ssl_credentials = SslCredentials().ssl_credentials - - # create a new channel. The provided one is ignored. - self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, - credentials_file=credentials_file, - ssl_credentials=ssl_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials - else: - host = host if ":" in host else host + ":443" + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, - ssl_credentials=ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -177,17 +170,8 @@ def __init__( ], ) - self._stubs = {} # type: Dict[str, Callable] - - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @classmethod def create_channel( @@ -201,7 +185,7 @@ def create_channel( ) -> grpc.Channel: """Create and return a gRPC channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If diff --git a/google/cloud/billing_v1/services/cloud_billing/transports/grpc_asyncio.py b/google/cloud/billing_v1/services/cloud_billing/transports/grpc_asyncio.py index acadeb9..a6b3ebc 100644 --- a/google/cloud/billing_v1/services/cloud_billing/transports/grpc_asyncio.py +++ b/google/cloud/billing_v1/services/cloud_billing/transports/grpc_asyncio.py @@ -64,7 +64,7 @@ def create_channel( ) -> aio.Channel: """Create and return a gRPC AsyncIO channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If @@ -104,6 +104,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id=None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -135,12 +136,16 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you're developing + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing your own client library. Raises: @@ -149,72 +154,60 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - warnings.warn( - "api_mtls_endpoint and client_cert_source are deprecated", - DeprecationWarning, - ) - host = ( - api_mtls_endpoint - if ":" in api_mtls_endpoint - else api_mtls_endpoint + ":443" - ) + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) - - # Create SSL credentials with client_cert_source or application - # default SSL credentials. - if client_cert_source: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) else: - ssl_credentials = SslCredentials().ssl_credentials - - # create a new channel. The provided one is ignored. - self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, - credentials_file=credentials_file, - ssl_credentials=ssl_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials - else: - host = host if ":" in host else host + ":443" + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, - ssl_credentials=ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -222,17 +215,8 @@ def __init__( ], ) - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) - - self._stubs = {} + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @property def grpc_channel(self) -> aio.Channel: diff --git a/google/cloud/billing_v1/services/cloud_catalog/async_client.py b/google/cloud/billing_v1/services/cloud_catalog/async_client.py index 4b95b43..31e91d7 100644 --- a/google/cloud/billing_v1/services/cloud_catalog/async_client.py +++ b/google/cloud/billing_v1/services/cloud_catalog/async_client.py @@ -77,7 +77,36 @@ class CloudCatalogAsyncClient: CloudCatalogClient.parse_common_location_path ) - from_service_account_file = CloudCatalogClient.from_service_account_file + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CloudCatalogAsyncClient: The constructed client. + """ + return CloudCatalogClient.from_service_account_info.__func__(CloudCatalogAsyncClient, info, *args, **kwargs) # type: ignore + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CloudCatalogAsyncClient: The constructed client. + """ + return CloudCatalogClient.from_service_account_file.__func__(CloudCatalogAsyncClient, filename, *args, **kwargs) # type: ignore + from_service_account_json = from_service_account_file @property @@ -152,7 +181,7 @@ async def list_services( r"""Lists all public cloud services. Args: - request (:class:`~.cloud_catalog.ListServicesRequest`): + request (:class:`google.cloud.billing_v1.types.ListServicesRequest`): The request object. Request message for `ListServices`. retry (google.api_core.retry.Retry): Designation of what errors, if any, @@ -162,8 +191,8 @@ async def list_services( sent along with the request as metadata. Returns: - ~.pagers.ListServicesAsyncPager: - Response message for ``ListServices``. + google.cloud.billing_v1.services.cloud_catalog.pagers.ListServicesAsyncPager: + Response message for ListServices. Iterating over this object will yield results and resolve additional pages automatically. @@ -206,11 +235,12 @@ async def list_skus( service. Args: - request (:class:`~.cloud_catalog.ListSkusRequest`): + request (:class:`google.cloud.billing_v1.types.ListSkusRequest`): The request object. Request message for `ListSkus`. parent (:class:`str`): Required. The name of the service. - Example: "services/DA34-426B-A397". + Example: "services/DA34-426B-A397" + This corresponds to the ``parent`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -222,8 +252,8 @@ async def list_skus( sent along with the request as metadata. Returns: - ~.pagers.ListSkusAsyncPager: - Response message for ``ListSkus``. + google.cloud.billing_v1.services.cloud_catalog.pagers.ListSkusAsyncPager: + Response message for ListSkus. Iterating over this object will yield results and resolve additional pages automatically. diff --git a/google/cloud/billing_v1/services/cloud_catalog/client.py b/google/cloud/billing_v1/services/cloud_catalog/client.py index c6f2851..eefb8eb 100644 --- a/google/cloud/billing_v1/services/cloud_catalog/client.py +++ b/google/cloud/billing_v1/services/cloud_catalog/client.py @@ -111,6 +111,22 @@ def _get_default_mtls_endpoint(api_endpoint): DEFAULT_ENDPOINT ) + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CloudCatalogClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + @classmethod def from_service_account_file(cls, filename: str, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -123,7 +139,7 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): kwargs: Additional arguments to pass to the constructor. Returns: - {@api.name}: The constructed client. + CloudCatalogClient: The constructed client. """ credentials = service_account.Credentials.from_service_account_file(filename) kwargs["credentials"] = credentials @@ -237,10 +253,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - transport (Union[str, ~.CloudCatalogTransport]): The + transport (Union[str, CloudCatalogTransport]): The transport to use. If set to None, a transport is chosen automatically. - client_options (client_options_lib.ClientOptions): Custom options for the + client_options (google.api_core.client_options.ClientOptions): Custom options for the client. It won't take effect if a ``transport`` instance is provided. (1) The ``api_endpoint`` property can be used to override the default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT @@ -276,21 +292,17 @@ def __init__( util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) ) - ssl_credentials = None + client_cert_source_func = None is_mtls = False if use_client_cert: if client_options.client_cert_source: - import grpc # type: ignore - - cert, key = client_options.client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) is_mtls = True + client_cert_source_func = client_options.client_cert_source else: - creds = SslCredentials() - is_mtls = creds.is_mtls - ssl_credentials = creds.ssl_credentials if is_mtls else None + is_mtls = mtls.has_default_client_cert_source() + client_cert_source_func = ( + mtls.default_client_cert_source() if is_mtls else None + ) # Figure out which api endpoint to use. if client_options.api_endpoint is not None: @@ -333,7 +345,7 @@ def __init__( credentials_file=client_options.credentials_file, host=api_endpoint, scopes=client_options.scopes, - ssl_channel_credentials=ssl_credentials, + client_cert_source_for_mtls=client_cert_source_func, quota_project_id=client_options.quota_project_id, client_info=client_info, ) @@ -349,7 +361,7 @@ def list_services( r"""Lists all public cloud services. Args: - request (:class:`~.cloud_catalog.ListServicesRequest`): + request (google.cloud.billing_v1.types.ListServicesRequest): The request object. Request message for `ListServices`. retry (google.api_core.retry.Retry): Designation of what errors, if any, @@ -359,8 +371,8 @@ def list_services( sent along with the request as metadata. Returns: - ~.pagers.ListServicesPager: - Response message for ``ListServices``. + google.cloud.billing_v1.services.cloud_catalog.pagers.ListServicesPager: + Response message for ListServices. Iterating over this object will yield results and resolve additional pages automatically. @@ -404,11 +416,12 @@ def list_skus( service. Args: - request (:class:`~.cloud_catalog.ListSkusRequest`): + request (google.cloud.billing_v1.types.ListSkusRequest): The request object. Request message for `ListSkus`. - parent (:class:`str`): + parent (str): Required. The name of the service. - Example: "services/DA34-426B-A397". + Example: "services/DA34-426B-A397" + This corresponds to the ``parent`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -420,8 +433,8 @@ def list_skus( sent along with the request as metadata. Returns: - ~.pagers.ListSkusPager: - Response message for ``ListSkus``. + google.cloud.billing_v1.services.cloud_catalog.pagers.ListSkusPager: + Response message for ListSkus. Iterating over this object will yield results and resolve additional pages automatically. diff --git a/google/cloud/billing_v1/services/cloud_catalog/pagers.py b/google/cloud/billing_v1/services/cloud_catalog/pagers.py index e9da024..99752d9 100644 --- a/google/cloud/billing_v1/services/cloud_catalog/pagers.py +++ b/google/cloud/billing_v1/services/cloud_catalog/pagers.py @@ -15,7 +15,16 @@ # limitations under the License. # -from typing import Any, AsyncIterable, Awaitable, Callable, Iterable, Sequence, Tuple +from typing import ( + Any, + AsyncIterable, + Awaitable, + Callable, + Iterable, + Sequence, + Tuple, + Optional, +) from google.cloud.billing_v1.types import cloud_catalog @@ -24,7 +33,7 @@ class ListServicesPager: """A pager for iterating through ``list_services`` requests. This class thinly wraps an initial - :class:`~.cloud_catalog.ListServicesResponse` object, and + :class:`google.cloud.billing_v1.types.ListServicesResponse` object, and provides an ``__iter__`` method to iterate through its ``services`` field. @@ -33,7 +42,7 @@ class ListServicesPager: through the ``services`` field on the corresponding responses. - All the usual :class:`~.cloud_catalog.ListServicesResponse` + All the usual :class:`google.cloud.billing_v1.types.ListServicesResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -51,9 +60,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_catalog.ListServicesRequest`): + request (google.cloud.billing_v1.types.ListServicesRequest): The initial request object. - response (:class:`~.cloud_catalog.ListServicesResponse`): + response (google.cloud.billing_v1.types.ListServicesResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. @@ -86,7 +95,7 @@ class ListServicesAsyncPager: """A pager for iterating through ``list_services`` requests. This class thinly wraps an initial - :class:`~.cloud_catalog.ListServicesResponse` object, and + :class:`google.cloud.billing_v1.types.ListServicesResponse` object, and provides an ``__aiter__`` method to iterate through its ``services`` field. @@ -95,7 +104,7 @@ class ListServicesAsyncPager: through the ``services`` field on the corresponding responses. - All the usual :class:`~.cloud_catalog.ListServicesResponse` + All the usual :class:`google.cloud.billing_v1.types.ListServicesResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -113,9 +122,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_catalog.ListServicesRequest`): + request (google.cloud.billing_v1.types.ListServicesRequest): The initial request object. - response (:class:`~.cloud_catalog.ListServicesResponse`): + response (google.cloud.billing_v1.types.ListServicesResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. @@ -152,7 +161,7 @@ class ListSkusPager: """A pager for iterating through ``list_skus`` requests. This class thinly wraps an initial - :class:`~.cloud_catalog.ListSkusResponse` object, and + :class:`google.cloud.billing_v1.types.ListSkusResponse` object, and provides an ``__iter__`` method to iterate through its ``skus`` field. @@ -161,7 +170,7 @@ class ListSkusPager: through the ``skus`` field on the corresponding responses. - All the usual :class:`~.cloud_catalog.ListSkusResponse` + All the usual :class:`google.cloud.billing_v1.types.ListSkusResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -179,9 +188,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_catalog.ListSkusRequest`): + request (google.cloud.billing_v1.types.ListSkusRequest): The initial request object. - response (:class:`~.cloud_catalog.ListSkusResponse`): + response (google.cloud.billing_v1.types.ListSkusResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. @@ -214,7 +223,7 @@ class ListSkusAsyncPager: """A pager for iterating through ``list_skus`` requests. This class thinly wraps an initial - :class:`~.cloud_catalog.ListSkusResponse` object, and + :class:`google.cloud.billing_v1.types.ListSkusResponse` object, and provides an ``__aiter__`` method to iterate through its ``skus`` field. @@ -223,7 +232,7 @@ class ListSkusAsyncPager: through the ``skus`` field on the corresponding responses. - All the usual :class:`~.cloud_catalog.ListSkusResponse` + All the usual :class:`google.cloud.billing_v1.types.ListSkusResponse` attributes are available on the pager. If multiple requests are made, only the most recent response is retained, and thus used for attribute lookup. """ @@ -241,9 +250,9 @@ def __init__( Args: method (Callable): The method that was originally called, and which instantiated this pager. - request (:class:`~.cloud_catalog.ListSkusRequest`): + request (google.cloud.billing_v1.types.ListSkusRequest): The initial request object. - response (:class:`~.cloud_catalog.ListSkusResponse`): + response (google.cloud.billing_v1.types.ListSkusResponse): The initial response object. metadata (Sequence[Tuple[str, str]]): Strings which should be sent along with the request as metadata. diff --git a/google/cloud/billing_v1/services/cloud_catalog/transports/base.py b/google/cloud/billing_v1/services/cloud_catalog/transports/base.py index 88276fa..9bd9b21 100644 --- a/google/cloud/billing_v1/services/cloud_catalog/transports/base.py +++ b/google/cloud/billing_v1/services/cloud_catalog/transports/base.py @@ -67,10 +67,10 @@ def __init__( scope (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you're developing + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing your own client library. """ # Save the hostname. Default to port 443 (HTTPS) if none is specified. @@ -78,6 +78,9 @@ def __init__( host += ":443" self._host = host + # Save the scopes. + self._scopes = scopes or self.AUTH_SCOPES + # If no credentials are provided, then determine the appropriate # defaults. if credentials and credentials_file: @@ -87,20 +90,17 @@ def __init__( if credentials_file is not None: credentials, _ = auth.load_credentials_from_file( - credentials_file, scopes=scopes, quota_project_id=quota_project_id + credentials_file, scopes=self._scopes, quota_project_id=quota_project_id ) elif credentials is None: credentials, _ = auth.default( - scopes=scopes, quota_project_id=quota_project_id + scopes=self._scopes, quota_project_id=quota_project_id ) # Save the credentials. self._credentials = credentials - # Lifted into its own function so it can be stubbed out during tests. - self._prep_wrapped_messages(client_info) - def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. self._wrapped_methods = { diff --git a/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py b/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py index f3e47c8..ab6a557 100644 --- a/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py +++ b/google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py @@ -59,6 +59,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id: Optional[str] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -89,6 +90,10 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. client_info (google.api_core.gapic_v1.client_info.ClientInfo): @@ -103,72 +108,60 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - warnings.warn( - "api_mtls_endpoint and client_cert_source are deprecated", - DeprecationWarning, - ) - host = ( - api_mtls_endpoint - if ":" in api_mtls_endpoint - else api_mtls_endpoint + ":443" - ) + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) - - # Create SSL credentials with client_cert_source or application - # default SSL credentials. - if client_cert_source: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) else: - ssl_credentials = SslCredentials().ssl_credentials - - # create a new channel. The provided one is ignored. - self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, - credentials_file=credentials_file, - ssl_credentials=ssl_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials - else: - host = host if ":" in host else host + ":443" + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, - ssl_credentials=ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -176,17 +169,8 @@ def __init__( ], ) - self._stubs = {} # type: Dict[str, Callable] - - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @classmethod def create_channel( @@ -200,7 +184,7 @@ def create_channel( ) -> grpc.Channel: """Create and return a gRPC channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If diff --git a/google/cloud/billing_v1/services/cloud_catalog/transports/grpc_asyncio.py b/google/cloud/billing_v1/services/cloud_catalog/transports/grpc_asyncio.py index 9d0e6a4..af6f4b7 100644 --- a/google/cloud/billing_v1/services/cloud_catalog/transports/grpc_asyncio.py +++ b/google/cloud/billing_v1/services/cloud_catalog/transports/grpc_asyncio.py @@ -63,7 +63,7 @@ def create_channel( ) -> aio.Channel: """Create and return a gRPC AsyncIO channel object. Args: - address (Optional[str]): The host for the channel to use. + host (Optional[str]): The host for the channel to use. credentials (Optional[~.Credentials]): The authorization credentials to attach to requests. These credentials identify this application to the service. If @@ -103,6 +103,7 @@ def __init__( api_mtls_endpoint: str = None, client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, quota_project_id=None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: @@ -134,12 +135,16 @@ def __init__( ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you're developing + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing your own client library. Raises: @@ -148,72 +153,60 @@ def __init__( google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` and ``credentials_file`` are passed. """ + self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) if channel: - # Sanity check: Ensure that channel and credentials are not both - # provided. + # Ignore credentials if a channel was passed. credentials = False - # If a channel was explicitly provided, set it. self._grpc_channel = channel self._ssl_channel_credentials = None - elif api_mtls_endpoint: - warnings.warn( - "api_mtls_endpoint and client_cert_source are deprecated", - DeprecationWarning, - ) - host = ( - api_mtls_endpoint - if ":" in api_mtls_endpoint - else api_mtls_endpoint + ":443" - ) + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) - - # Create SSL credentials with client_cert_source or application - # default SSL credentials. - if client_cert_source: - cert, key = client_cert_source() - ssl_credentials = grpc.ssl_channel_credentials( - certificate_chain=cert, private_key=key - ) else: - ssl_credentials = SslCredentials().ssl_credentials - - # create a new channel. The provided one is ignored. - self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, - credentials_file=credentials_file, - ssl_credentials=ssl_credentials, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - options=[ - ("grpc.max_send_message_length", -1), - ("grpc.max_receive_message_length", -1), - ], - ) - self._ssl_channel_credentials = ssl_credentials - else: - host = host if ":" in host else host + ":443" + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) - if credentials is None: - credentials, _ = auth.default( - scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id - ) + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + ) - # create a new channel. The provided one is ignored. + if not self._grpc_channel: self._grpc_channel = type(self).create_channel( - host, - credentials=credentials, + self._host, + credentials=self._credentials, credentials_file=credentials_file, - ssl_credentials=ssl_channel_credentials, - scopes=scopes or self.AUTH_SCOPES, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, options=[ ("grpc.max_send_message_length", -1), @@ -221,17 +214,8 @@ def __init__( ], ) - # Run the base constructor. - super().__init__( - host=host, - credentials=credentials, - credentials_file=credentials_file, - scopes=scopes or self.AUTH_SCOPES, - quota_project_id=quota_project_id, - client_info=client_info, - ) - - self._stubs = {} + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) @property def grpc_channel(self) -> aio.Channel: diff --git a/google/cloud/billing_v1/types/__init__.py b/google/cloud/billing_v1/types/__init__.py index a0dbe47..43a8f93 100644 --- a/google/cloud/billing_v1/types/__init__.py +++ b/google/cloud/billing_v1/types/__init__.py @@ -17,50 +17,50 @@ from .cloud_billing import ( BillingAccount, - ProjectBillingInfo, + CreateBillingAccountRequest, GetBillingAccountRequest, + GetProjectBillingInfoRequest, ListBillingAccountsRequest, ListBillingAccountsResponse, - CreateBillingAccountRequest, - UpdateBillingAccountRequest, ListProjectBillingInfoRequest, ListProjectBillingInfoResponse, - GetProjectBillingInfoRequest, + ProjectBillingInfo, + UpdateBillingAccountRequest, UpdateProjectBillingInfoRequest, ) from .cloud_catalog import ( - Service, - Sku, - Category, - PricingInfo, - PricingExpression, AggregationInfo, + Category, ListServicesRequest, ListServicesResponse, ListSkusRequest, ListSkusResponse, + PricingExpression, + PricingInfo, + Service, + Sku, ) __all__ = ( "BillingAccount", - "ProjectBillingInfo", + "CreateBillingAccountRequest", "GetBillingAccountRequest", + "GetProjectBillingInfoRequest", "ListBillingAccountsRequest", "ListBillingAccountsResponse", - "CreateBillingAccountRequest", - "UpdateBillingAccountRequest", "ListProjectBillingInfoRequest", "ListProjectBillingInfoResponse", - "GetProjectBillingInfoRequest", + "ProjectBillingInfo", + "UpdateBillingAccountRequest", "UpdateProjectBillingInfoRequest", - "Service", - "Sku", - "Category", - "PricingInfo", - "PricingExpression", "AggregationInfo", + "Category", "ListServicesRequest", "ListServicesResponse", "ListSkusRequest", "ListSkusResponse", + "PricingExpression", + "PricingInfo", + "Service", + "Sku", ) diff --git a/google/cloud/billing_v1/types/cloud_billing.py b/google/cloud/billing_v1/types/cloud_billing.py index 5b1090d..23e03b1 100644 --- a/google/cloud/billing_v1/types/cloud_billing.py +++ b/google/cloud/billing_v1/types/cloud_billing.py @@ -167,7 +167,7 @@ class ListBillingAccountsResponse(proto.Message): r"""Response message for ``ListBillingAccounts``. Attributes: - billing_accounts (Sequence[~.cloud_billing.BillingAccount]): + billing_accounts (Sequence[google.cloud.billing_v1.types.BillingAccount]): A list of billing accounts. next_page_token (str): A token to retrieve the next page of results. To retrieve @@ -191,7 +191,7 @@ class CreateBillingAccountRequest(proto.Message): r"""Request message for ``CreateBillingAccount``. Attributes: - billing_account (~.cloud_billing.BillingAccount): + billing_account (google.cloud.billing_v1.types.BillingAccount): Required. The billing account resource to create. Currently CreateBillingAccount only supports subaccount creation, so any created @@ -209,10 +209,10 @@ class UpdateBillingAccountRequest(proto.Message): name (str): Required. The name of the billing account resource to be updated. - account (~.cloud_billing.BillingAccount): + account (google.cloud.billing_v1.types.BillingAccount): Required. The billing account resource to replace the resource on the server. - update_mask (~.field_mask.FieldMask): + update_mask (google.protobuf.field_mask_pb2.FieldMask): The update mask applied to the resource. Only "display_name" is currently supported. """ @@ -253,7 +253,7 @@ class ListProjectBillingInfoResponse(proto.Message): r"""Request message for ``ListProjectBillingInfoResponse``. Attributes: - project_billing_info (Sequence[~.cloud_billing.ProjectBillingInfo]): + project_billing_info (Sequence[google.cloud.billing_v1.types.ProjectBillingInfo]): A list of ``ProjectBillingInfo`` resources representing the projects associated with the billing account. next_page_token (str): @@ -295,7 +295,7 @@ class UpdateProjectBillingInfoRequest(proto.Message): Required. The resource name of the project associated with the billing information that you want to update. For example, ``projects/tokyo-rain-123``. - project_billing_info (~.cloud_billing.ProjectBillingInfo): + project_billing_info (google.cloud.billing_v1.types.ProjectBillingInfo): The new billing information for the project. Read-only fields are ignored; thus, you can leave empty all fields except ``billing_account_name``. diff --git a/google/cloud/billing_v1/types/cloud_catalog.py b/google/cloud/billing_v1/types/cloud_catalog.py index 36278e8..09f9a39 100644 --- a/google/cloud/billing_v1/types/cloud_catalog.py +++ b/google/cloud/billing_v1/types/cloud_catalog.py @@ -81,7 +81,7 @@ class Sku(proto.Message): description (str): A human readable description of the SKU, has a maximum length of 256 characters. - category (~.cloud_catalog.Category): + category (google.cloud.billing_v1.types.Category): The category hierarchy of this SKU, purely for organizational purpose. service_regions (Sequence[str]): @@ -89,7 +89,7 @@ class Sku(proto.Message): at. Example: "asia-east1" Service regions can be found at https://cloud.google.com/about/locations/ - pricing_info (Sequence[~.cloud_catalog.PricingInfo]): + pricing_info (Sequence[google.cloud.billing_v1.types.PricingInfo]): A timeline of pricing info for this SKU in chronological order. service_provider_name (str): @@ -148,7 +148,7 @@ class PricingInfo(proto.Message): point of time. Attributes: - effective_time (~.timestamp.Timestamp): + effective_time (google.protobuf.timestamp_pb2.Timestamp): The timestamp from which this pricing was effective within the requested time range. This is guaranteed to be greater than or equal to the start_time field in the request and @@ -160,10 +160,10 @@ class PricingInfo(proto.Message): An optional human readable summary of the pricing information, has a maximum length of 256 characters. - pricing_expression (~.cloud_catalog.PricingExpression): + pricing_expression (google.cloud.billing_v1.types.PricingExpression): Expresses the pricing formula. See ``PricingExpression`` for an example. - aggregation_info (~.cloud_catalog.AggregationInfo): + aggregation_info (google.cloud.billing_v1.types.AggregationInfo): Aggregation Info. This can be left unspecified if the pricing expression doesn't require aggregation. @@ -229,7 +229,7 @@ class PricingExpression(proto.Message): If the unit_price is "0.0001 USD", the usage_unit is "GB" and the display_quantity is "1000" then the recommended way of displaying the pricing info is "0.10 USD per 1000 GB". - tiered_rates (Sequence[~.cloud_catalog.PricingExpression.TierRate]): + tiered_rates (Sequence[google.cloud.billing_v1.types.PricingExpression.TierRate]): The list of tiered rates for this pricing. The total cost is computed by applying each of the tiered rates on usage. This repeated list is sorted by ascending order of @@ -246,7 +246,7 @@ class TierRate(proto.Message): Example: start_usage_amount of 10 indicates that the usage will be priced at the unit_price after the first 10 usage_units. - unit_price (~.money.Money): + unit_price (google.type.money_pb2.Money): The price per unit of usage. Example: unit_price of amount $10 indicates that each unit will cost $10. """ @@ -275,9 +275,9 @@ class AggregationInfo(proto.Message): a single SKU. Attributes: - aggregation_level (~.cloud_catalog.AggregationInfo.AggregationLevel): + aggregation_level (google.cloud.billing_v1.types.AggregationInfo.AggregationLevel): - aggregation_interval (~.cloud_catalog.AggregationInfo.AggregationInterval): + aggregation_interval (google.cloud.billing_v1.types.AggregationInfo.AggregationInterval): aggregation_count (int): The number of intervals to aggregate over. Example: If @@ -333,7 +333,7 @@ class ListServicesResponse(proto.Message): r"""Response message for ``ListServices``. Attributes: - services (Sequence[~.cloud_catalog.Service]): + services (Sequence[google.cloud.billing_v1.types.Service]): A list of services. next_page_token (str): A token to retrieve the next page of results. To retrieve @@ -358,14 +358,14 @@ class ListSkusRequest(proto.Message): parent (str): Required. The name of the service. Example: "services/DA34-426B-A397". - start_time (~.timestamp.Timestamp): + start_time (google.protobuf.timestamp_pb2.Timestamp): Optional inclusive start time of the time range for which the pricing versions will be returned. Timestamps in the future are not allowed. The time range has to be within a single calendar month in America/Los_Angeles timezone. Time range as a whole is optional. If not specified, the latest pricing will be returned (up to 12 hours old at most). - end_time (~.timestamp.Timestamp): + end_time (google.protobuf.timestamp_pb2.Timestamp): Optional exclusive end time of the time range for which the pricing versions will be returned. Timestamps in the future are not allowed. The time range has to be within a single @@ -402,7 +402,7 @@ class ListSkusResponse(proto.Message): r"""Response message for ``ListSkus``. Attributes: - skus (Sequence[~.cloud_catalog.Sku]): + skus (Sequence[google.cloud.billing_v1.types.Sku]): The list of public SKUs of the given service. next_page_token (str): A token to retrieve the next page of results. To retrieve diff --git a/noxfile.py b/noxfile.py index 70d9c13..4d37cd3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -18,6 +18,7 @@ from __future__ import absolute_import import os +import pathlib import shutil import nox @@ -30,6 +31,8 @@ SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"] UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"] +CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() + # 'docfx' is excluded since it only needs to run in 'docs-presubmit' nox.options.sessions = [ "unit", @@ -41,6 +44,9 @@ "docs", ] +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + @nox.session(python=DEFAULT_PYTHON_VERSION) def lint(session): @@ -81,18 +87,21 @@ def lint_setup_py(session): def default(session): # Install all test dependencies, then install this package in-place. - session.install("asyncmock", "pytest-asyncio") - session.install( - "mock", "pytest", "pytest-cov", + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) + session.install("asyncmock", "pytest-asyncio", "-c", constraints_path) - session.install("-e", ".") + session.install("mock", "pytest", "pytest-cov", "-c", constraints_path) + + session.install("-e", ".", "-c", constraints_path) # Run py.test against the unit tests. session.run( "py.test", "--quiet", + f"--junitxml=unit_{session.python}_sponge_log.xml", "--cov=google/cloud", "--cov=tests/unit", "--cov-append", @@ -113,6 +122,9 @@ def unit(session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def system(session): """Run the system test suite.""" + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) system_test_path = os.path.join("tests", "system.py") system_test_folder_path = os.path.join("tests", "system") @@ -122,6 +134,9 @@ def system(session): # Sanity check: Only run tests if the environment variable is set. if not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", ""): session.skip("Credentials must be set via environment variable") + # Install pyopenssl for mTLS testing. + if os.environ.get("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true": + session.install("pyopenssl") system_test_exists = os.path.exists(system_test_path) system_test_folder_exists = os.path.exists(system_test_folder_path) @@ -134,16 +149,26 @@ def system(session): # Install all test dependencies, then install this package into the # virtualenv's dist-packages. - session.install( - "mock", "pytest", "google-cloud-testutils", - ) - session.install("-e", ".") + session.install("mock", "pytest", "google-cloud-testutils", "-c", constraints_path) + session.install("-e", ".", "-c", constraints_path) # Run py.test against the system tests. if system_test_exists: - session.run("py.test", "--quiet", system_test_path, *session.posargs) + session.run( + "py.test", + "--quiet", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_path, + *session.posargs, + ) if system_test_folder_exists: - session.run("py.test", "--quiet", system_test_folder_path, *session.posargs) + session.run( + "py.test", + "--quiet", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_folder_path, + *session.posargs, + ) @nox.session(python=DEFAULT_PYTHON_VERSION) @@ -154,7 +179,7 @@ def cover(session): test runs (not system test runs), and then erases coverage data. """ session.install("coverage", "pytest-cov") - session.run("coverage", "report", "--show-missing", "--fail-under=99") + session.run("coverage", "report", "--show-missing", "--fail-under=98") session.run("coverage", "erase") @@ -186,9 +211,7 @@ def docfx(session): """Build the docfx yaml files for this library.""" session.install("-e", ".") - # sphinx-docfx-yaml supports up to sphinx version 1.5.5. - # https://github.com/docascode/sphinx-docfx-yaml/issues/97 - session.install("sphinx==1.5.5", "alabaster", "recommonmark", "sphinx-docfx-yaml") + session.install("sphinx", "alabaster", "recommonmark", "gcp-sphinx-docfx-yaml") shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) session.run( diff --git a/renovate.json b/renovate.json index 4fa9493..f08bc22 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,6 @@ { "extends": [ "config:base", ":preserveSemverRanges" - ] + ], + "ignorePaths": [".pre-commit-config.yaml"] } diff --git a/scripts/fixup_billing_v1_keywords.py b/scripts/fixup_billing_v1_keywords.py deleted file mode 100644 index dcee79c..0000000 --- a/scripts/fixup_billing_v1_keywords.py +++ /dev/null @@ -1,190 +0,0 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import argparse -import os -import libcst as cst -import pathlib -import sys -from typing import (Any, Callable, Dict, List, Sequence, Tuple) - - -def partition( - predicate: Callable[[Any], bool], - iterator: Sequence[Any] -) -> Tuple[List[Any], List[Any]]: - """A stable, out-of-place partition.""" - results = ([], []) - - for i in iterator: - results[int(predicate(i))].append(i) - - # Returns trueList, falseList - return results[1], results[0] - - -class billingCallTransformer(cst.CSTTransformer): - CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') - METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { - 'create_billing_account': ('billing_account', ), - 'get_billing_account': ('name', ), - 'get_iam_policy': ('resource', 'options', ), - 'get_project_billing_info': ('name', ), - 'list_billing_accounts': ('page_size', 'page_token', 'filter', ), - 'list_project_billing_info': ('name', 'page_size', 'page_token', ), - 'list_services': ('page_size', 'page_token', ), - 'list_skus': ('parent', 'start_time', 'end_time', 'currency_code', 'page_size', 'page_token', ), - 'set_iam_policy': ('resource', 'policy', ), - 'test_iam_permissions': ('resource', 'permissions', ), - 'update_billing_account': ('name', 'account', 'update_mask', ), - 'update_project_billing_info': ('name', 'project_billing_info', ), - - } - - def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode: - try: - key = original.func.attr.value - kword_params = self.METHOD_TO_PARAMS[key] - except (AttributeError, KeyError): - # Either not a method from the API or too convoluted to be sure. - return updated - - # If the existing code is valid, keyword args come after positional args. - # Therefore, all positional args must map to the first parameters. - args, kwargs = partition(lambda a: not bool(a.keyword), updated.args) - if any(k.keyword.value == "request" for k in kwargs): - # We've already fixed this file, don't fix it again. - return updated - - kwargs, ctrl_kwargs = partition( - lambda a: not a.keyword.value in self.CTRL_PARAMS, - kwargs - ) - - args, ctrl_args = args[:len(kword_params)], args[len(kword_params):] - ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl)) - for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS)) - - request_arg = cst.Arg( - value=cst.Dict([ - cst.DictElement( - cst.SimpleString("'{}'".format(name)), - cst.Element(value=arg.value) - ) - # Note: the args + kwargs looks silly, but keep in mind that - # the control parameters had to be stripped out, and that - # those could have been passed positionally or by keyword. - for name, arg in zip(kword_params, args + kwargs)]), - keyword=cst.Name("request") - ) - - return updated.with_changes( - args=[request_arg] + ctrl_kwargs - ) - - -def fix_files( - in_dir: pathlib.Path, - out_dir: pathlib.Path, - *, - transformer=billingCallTransformer(), -): - """Duplicate the input dir to the output dir, fixing file method calls. - - Preconditions: - * in_dir is a real directory - * out_dir is a real, empty directory - """ - pyfile_gen = ( - pathlib.Path(os.path.join(root, f)) - for root, _, files in os.walk(in_dir) - for f in files if os.path.splitext(f)[1] == ".py" - ) - - for fpath in pyfile_gen: - with open(fpath, 'r') as f: - src = f.read() - - # Parse the code and insert method call fixes. - tree = cst.parse_module(src) - updated = tree.visit(transformer) - - # Create the path and directory structure for the new file. - updated_path = out_dir.joinpath(fpath.relative_to(in_dir)) - updated_path.parent.mkdir(parents=True, exist_ok=True) - - # Generate the updated source file at the corresponding path. - with open(updated_path, 'w') as f: - f.write(updated.code) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="""Fix up source that uses the billing client library. - -The existing sources are NOT overwritten but are copied to output_dir with changes made. - -Note: This tool operates at a best-effort level at converting positional - parameters in client method calls to keyword based parameters. - Cases where it WILL FAIL include - A) * or ** expansion in a method call. - B) Calls via function or method alias (includes free function calls) - C) Indirect or dispatched calls (e.g. the method is looked up dynamically) - - These all constitute false negatives. The tool will also detect false - positives when an API method shares a name with another method. -""") - parser.add_argument( - '-d', - '--input-directory', - required=True, - dest='input_dir', - help='the input directory to walk for python files to fix up', - ) - parser.add_argument( - '-o', - '--output-directory', - required=True, - dest='output_dir', - help='the directory to output files fixed via un-flattening', - ) - args = parser.parse_args() - input_dir = pathlib.Path(args.input_dir) - output_dir = pathlib.Path(args.output_dir) - if not input_dir.is_dir(): - print( - f"input directory '{input_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if not output_dir.is_dir(): - print( - f"output directory '{output_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if os.listdir(output_dir): - print( - f"output directory '{output_dir}' is not empty", - file=sys.stderr, - ) - sys.exit(-1) - - fix_files(input_dir, output_dir) diff --git a/scripts/fixup_keywords.py b/scripts/fixup_keywords.py deleted file mode 100644 index 3639889..0000000 --- a/scripts/fixup_keywords.py +++ /dev/null @@ -1,188 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import argparse -import os -import libcst as cst -import pathlib -import sys -from typing import (Any, Callable, Dict, List, Sequence, Tuple) - - -def partition( - predicate: Callable[[Any], bool], - iterator: Sequence[Any] -) -> Tuple[List[Any], List[Any]]: - """A stable, out-of-place partition.""" - results = ([], []) - - for i in iterator: - results[int(predicate(i))].append(i) - - # Returns trueList, falseList - return results[1], results[0] - - -class billingCallTransformer(cst.CSTTransformer): - CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') - METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { - 'create_billing_account': ('billing_account', ), - 'get_billing_account': ('name', ), - 'get_iam_policy': ('resource', 'options', ), - 'get_project_billing_info': ('name', ), - 'list_billing_accounts': ('page_size', 'page_token', 'filter', ), - 'list_project_billing_info': ('name', 'page_size', 'page_token', ), - 'list_services': ('page_size', 'page_token', ), - 'list_skus': ('parent', 'start_time', 'end_time', 'currency_code', 'page_size', 'page_token', ), - 'set_iam_policy': ('resource', 'policy', ), - 'test_iam_permissions': ('resource', 'permissions', ), - 'update_billing_account': ('name', 'account', 'update_mask', ), - 'update_project_billing_info': ('name', 'project_billing_info', ), - } - - def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode: - try: - key = original.func.attr.value - kword_params = self.METHOD_TO_PARAMS[key] - except (AttributeError, KeyError): - # Either not a method from the API or too convoluted to be sure. - return updated - - # If the existing code is valid, keyword args come after positional args. - # Therefore, all positional args must map to the first parameters. - args, kwargs = partition(lambda a: not bool(a.keyword), updated.args) - if any(k.keyword.value == "request" for k in kwargs): - # We've already fixed this file, don't fix it again. - return updated - - kwargs, ctrl_kwargs = partition( - lambda a: not a.keyword.value in self.CTRL_PARAMS, - kwargs - ) - - args, ctrl_args = args[:len(kword_params)], args[len(kword_params):] - ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl)) - for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS)) - - request_arg = cst.Arg( - value=cst.Dict([ - cst.DictElement( - cst.SimpleString("'{}'".format(name)), - cst.Element(value=arg.value) - ) - # Note: the args + kwargs looks silly, but keep in mind that - # the control parameters had to be stripped out, and that - # those could have been passed positionally or by keyword. - for name, arg in zip(kword_params, args + kwargs)]), - keyword=cst.Name("request") - ) - - return updated.with_changes( - args=[request_arg] + ctrl_kwargs - ) - - -def fix_files( - in_dir: pathlib.Path, - out_dir: pathlib.Path, - *, - transformer=billingCallTransformer(), -): - """Duplicate the input dir to the output dir, fixing file method calls. - - Preconditions: - * in_dir is a real directory - * out_dir is a real, empty directory - """ - pyfile_gen = ( - pathlib.Path(os.path.join(root, f)) - for root, _, files in os.walk(in_dir) - for f in files if os.path.splitext(f)[1] == ".py" - ) - - for fpath in pyfile_gen: - with open(fpath, 'r') as f: - src = f.read() - - # Parse the code and insert method call fixes. - tree = cst.parse_module(src) - updated = tree.visit(transformer) - - # Create the path and directory structure for the new file. - updated_path = out_dir.joinpath(fpath.relative_to(in_dir)) - updated_path.parent.mkdir(parents=True, exist_ok=True) - - # Generate the updated source file at the corresponding path. - with open(updated_path, 'w') as f: - f.write(updated.code) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="""Fix up source that uses the billing client library. - -The existing sources are NOT overwritten but are copied to output_dir with changes made. - -Note: This tool operates at a best-effort level at converting positional - parameters in client method calls to keyword based parameters. - Cases where it WILL FAIL include - A) * or ** expansion in a method call. - B) Calls via function or method alias (includes free function calls) - C) Indirect or dispatched calls (e.g. the method is looked up dynamically) - - These all constitute false negatives. The tool will also detect false - positives when an API method shares a name with another method. -""") - parser.add_argument( - '-d', - '--input-directory', - required=True, - dest='input_dir', - help='the input directory to walk for python files to fix up', - ) - parser.add_argument( - '-o', - '--output-directory', - required=True, - dest='output_dir', - help='the directory to output files fixed via un-flattening', - ) - args = parser.parse_args() - input_dir = pathlib.Path(args.input_dir) - output_dir = pathlib.Path(args.output_dir) - if not input_dir.is_dir(): - print( - f"input directory '{input_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if not output_dir.is_dir(): - print( - f"output directory '{output_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if os.listdir(output_dir): - print( - f"output directory '{output_dir}' is not empty", - file=sys.stderr, - ) - sys.exit(-1) - - fix_files(input_dir, output_dir) diff --git a/setup.py b/setup.py index 8e07725..8668d8e 100644 --- a/setup.py +++ b/setup.py @@ -40,13 +40,11 @@ platforms="Posix; MacOS X; Windows", include_package_data=True, install_requires=( - "google-api-core[grpc] >= 1.22.0, < 2.0.0dev", - "grpc-google-iam-v1", + "google-api-core[grpc] >= 1.22.2, < 2.0.0dev", + "grpc-google-iam-v1 >= 0.12.3, < 0.13.0", "proto-plus >= 1.10.0", ), python_requires=">=3.6", - setup_requires=["libcst >= 0.2.5"], - scripts=["scripts/fixup_keywords.py"], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", diff --git a/synth.metadata b/synth.metadata index 70f6093..e3e0e24 100644 --- a/synth.metadata +++ b/synth.metadata @@ -3,30 +3,30 @@ { "git": { "name": ".", - "remote": "https://github.com/googleapis/python-billing.git", - "sha": "65755f9cb54c745a6f9bdd4e23a339e2168eb383" + "remote": "git@github.com:googleapis/python-billing", + "sha": "2d773138c9d49d45cbc4c74add003adaceef4892" } }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", - "sha": "69697504d9eba1d064820c3085b4750767be6d08", - "internalRef": "348952930" + "sha": "56fc6d43fed71188d7e18f3ca003544646c4ab35", + "internalRef": "366346972" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "4679e7e415221f03ff2a71e3ffad75b9ec41d87e" + "sha": "6d76df2138f8f841e5a5b9ac427f81def520c15f" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "4679e7e415221f03ff2a71e3ffad75b9ec41d87e" + "sha": "6d76df2138f8f841e5a5b9ac427f81def520c15f" } } ], @@ -40,103 +40,5 @@ "generator": "bazel" } } - ], - "generatedFiles": [ - ".flake8", - ".github/CONTRIBUTING.md", - ".github/ISSUE_TEMPLATE/bug_report.md", - ".github/ISSUE_TEMPLATE/feature_request.md", - ".github/ISSUE_TEMPLATE/support_request.md", - ".github/PULL_REQUEST_TEMPLATE.md", - ".github/header-checker-lint.yml", - ".github/release-please.yml", - ".github/snippet-bot.yml", - ".gitignore", - ".kokoro/build.sh", - ".kokoro/continuous/common.cfg", - ".kokoro/continuous/continuous.cfg", - ".kokoro/docker/docs/Dockerfile", - ".kokoro/docker/docs/fetch_gpg_keys.sh", - ".kokoro/docs/common.cfg", - ".kokoro/docs/docs-presubmit.cfg", - ".kokoro/docs/docs.cfg", - ".kokoro/populate-secrets.sh", - ".kokoro/presubmit/common.cfg", - ".kokoro/presubmit/presubmit.cfg", - ".kokoro/publish-docs.sh", - ".kokoro/release.sh", - ".kokoro/release/common.cfg", - ".kokoro/release/release.cfg", - ".kokoro/samples/lint/common.cfg", - ".kokoro/samples/lint/continuous.cfg", - ".kokoro/samples/lint/periodic.cfg", - ".kokoro/samples/lint/presubmit.cfg", - ".kokoro/samples/python3.6/common.cfg", - ".kokoro/samples/python3.6/continuous.cfg", - ".kokoro/samples/python3.6/periodic.cfg", - ".kokoro/samples/python3.6/presubmit.cfg", - ".kokoro/samples/python3.7/common.cfg", - ".kokoro/samples/python3.7/continuous.cfg", - ".kokoro/samples/python3.7/periodic.cfg", - ".kokoro/samples/python3.7/presubmit.cfg", - ".kokoro/samples/python3.8/common.cfg", - ".kokoro/samples/python3.8/continuous.cfg", - ".kokoro/samples/python3.8/periodic.cfg", - ".kokoro/samples/python3.8/presubmit.cfg", - ".kokoro/test-samples.sh", - ".kokoro/trampoline.sh", - ".kokoro/trampoline_v2.sh", - ".pre-commit-config.yaml", - ".trampolinerc", - "CODE_OF_CONDUCT.md", - "CONTRIBUTING.rst", - "LICENSE", - "MANIFEST.in", - "docs/_static/custom.css", - "docs/_templates/layout.html", - "docs/billing_v1/services.rst", - "docs/billing_v1/types.rst", - "docs/conf.py", - "docs/multiprocessing.rst", - "google/cloud/billing/__init__.py", - "google/cloud/billing/py.typed", - "google/cloud/billing_v1/__init__.py", - "google/cloud/billing_v1/py.typed", - "google/cloud/billing_v1/services/__init__.py", - "google/cloud/billing_v1/services/cloud_billing/__init__.py", - "google/cloud/billing_v1/services/cloud_billing/async_client.py", - "google/cloud/billing_v1/services/cloud_billing/client.py", - "google/cloud/billing_v1/services/cloud_billing/pagers.py", - "google/cloud/billing_v1/services/cloud_billing/transports/__init__.py", - "google/cloud/billing_v1/services/cloud_billing/transports/base.py", - "google/cloud/billing_v1/services/cloud_billing/transports/grpc.py", - "google/cloud/billing_v1/services/cloud_billing/transports/grpc_asyncio.py", - "google/cloud/billing_v1/services/cloud_catalog/__init__.py", - "google/cloud/billing_v1/services/cloud_catalog/async_client.py", - "google/cloud/billing_v1/services/cloud_catalog/client.py", - "google/cloud/billing_v1/services/cloud_catalog/pagers.py", - "google/cloud/billing_v1/services/cloud_catalog/transports/__init__.py", - "google/cloud/billing_v1/services/cloud_catalog/transports/base.py", - "google/cloud/billing_v1/services/cloud_catalog/transports/grpc.py", - "google/cloud/billing_v1/services/cloud_catalog/transports/grpc_asyncio.py", - "google/cloud/billing_v1/types/__init__.py", - "google/cloud/billing_v1/types/cloud_billing.py", - "google/cloud/billing_v1/types/cloud_catalog.py", - "mypy.ini", - "noxfile.py", - "renovate.json", - "scripts/decrypt-secrets.sh", - "scripts/fixup_billing_v1_keywords.py", - "scripts/readme-gen/readme_gen.py", - "scripts/readme-gen/templates/README.tmpl.rst", - "scripts/readme-gen/templates/auth.tmpl.rst", - "scripts/readme-gen/templates/auth_api_key.tmpl.rst", - "scripts/readme-gen/templates/install_deps.tmpl.rst", - "scripts/readme-gen/templates/install_portaudio.tmpl.rst", - "setup.cfg", - "testing/.gitignore", - "tests/unit/gapic/billing_v1/__init__.py", - "tests/unit/gapic/billing_v1/test_cloud_billing.py", - "tests/unit/gapic/billing_v1/test_cloud_catalog.py" ] } \ No newline at end of file diff --git a/synth.py b/synth.py index 0d66b85..427b080 100644 --- a/synth.py +++ b/synth.py @@ -31,13 +31,13 @@ bazel_target="//google/cloud/billing/v1:billing-v1-py", ) -excludes = ["setup.py", "docs/index.rst", "scripts/fixup_biling_v1_keywords.py"] +excludes = ["setup.py", "docs/index.rst", "scripts/fixup*"] s.move(library, excludes=excludes) # ---------------------------------------------------------------------------- # Add templated files # ---------------------------------------------------------------------------- -templated_files = common.py_library(cov_level=99, microgenerator=True) +templated_files = common.py_library(cov_level=98, microgenerator=True) s.move(templated_files, excludes=[".coveragerc"]) # the microgenerator has a good coveragerc file s.replace(".gitignore", "bigquery/docs/generated", "htmlcov") # temporary hack to ignore htmlcov diff --git a/testing/constraints-3.6.txt b/testing/constraints-3.6.txt index 21a8dc7..3a7d1dc 100644 --- a/testing/constraints-3.6.txt +++ b/testing/constraints-3.6.txt @@ -5,6 +5,6 @@ # # e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev", # Then this file should have foo==1.14.0 -google-api-core==1.22.0 +google-api-core==1.22.2 grpc-google-iam-v1==0.12.3 proto-plus==1.10.0 diff --git a/tests/unit/gapic/billing_v1/__init__.py b/tests/unit/gapic/billing_v1/__init__.py index 8b13789..42ffdf2 100644 --- a/tests/unit/gapic/billing_v1/__init__.py +++ b/tests/unit/gapic/billing_v1/__init__.py @@ -1 +1,16 @@ +# -*- coding: utf-8 -*- +# 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. +# diff --git a/tests/unit/gapic/billing_v1/test_cloud_billing.py b/tests/unit/gapic/billing_v1/test_cloud_billing.py index 6ee2c15..0a94fec 100644 --- a/tests/unit/gapic/billing_v1/test_cloud_billing.py +++ b/tests/unit/gapic/billing_v1/test_cloud_billing.py @@ -86,7 +86,22 @@ def test__get_default_mtls_endpoint(): assert CloudBillingClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi -@pytest.mark.parametrize("client_class", [CloudBillingClient, CloudBillingAsyncClient]) +@pytest.mark.parametrize("client_class", [CloudBillingClient, CloudBillingAsyncClient,]) +def test_cloud_billing_client_from_service_account_info(client_class): + creds = credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = client_class.from_service_account_info(info) + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "cloudbilling.googleapis.com:443" + + +@pytest.mark.parametrize("client_class", [CloudBillingClient, CloudBillingAsyncClient,]) def test_cloud_billing_client_from_service_account_file(client_class): creds = credentials.AnonymousCredentials() with mock.patch.object( @@ -95,16 +110,21 @@ def test_cloud_billing_client_from_service_account_file(client_class): factory.return_value = creds client = client_class.from_service_account_file("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) client = client_class.from_service_account_json("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) assert client.transport._host == "cloudbilling.googleapis.com:443" def test_cloud_billing_client_get_transport_class(): transport = CloudBillingClient.get_transport_class() - assert transport == transports.CloudBillingGrpcTransport + available_transports = [ + transports.CloudBillingGrpcTransport, + ] + assert transport in available_transports transport = CloudBillingClient.get_transport_class("grpc") assert transport == transports.CloudBillingGrpcTransport @@ -153,7 +173,7 @@ def test_cloud_billing_client_client_options( credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -169,7 +189,7 @@ def test_cloud_billing_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -185,7 +205,7 @@ def test_cloud_billing_client_client_options( credentials_file=None, host=client.DEFAULT_MTLS_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -213,7 +233,7 @@ def test_cloud_billing_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id="octopus", client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -262,29 +282,25 @@ def test_cloud_billing_client_mtls_env_auto( client_cert_source=client_cert_source_callback ) with mock.patch.object(transport_class, "__init__") as patched: - ssl_channel_creds = mock.Mock() - with mock.patch( - "grpc.ssl_channel_credentials", return_value=ssl_channel_creds - ): - patched.return_value = None - client = client_class(client_options=options) + patched.return_value = None + client = client_class(client_options=options) - if use_client_cert_env == "false": - expected_ssl_channel_creds = None - expected_host = client.DEFAULT_ENDPOINT - else: - expected_ssl_channel_creds = ssl_channel_creds - expected_host = client.DEFAULT_MTLS_ENDPOINT + if use_client_cert_env == "false": + expected_client_cert_source = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_client_cert_source = client_cert_source_callback + expected_host = client.DEFAULT_MTLS_ENDPOINT - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=expected_host, - scopes=None, - ssl_channel_credentials=expected_ssl_channel_creds, - quota_project_id=None, - client_info=transports.base.DEFAULT_CLIENT_INFO, - ) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) # Check the case ADC client cert is provided. Whether client cert is used depends on # GOOGLE_API_USE_CLIENT_CERTIFICATE value. @@ -293,66 +309,53 @@ def test_cloud_billing_client_mtls_env_auto( ): with mock.patch.object(transport_class, "__init__") as patched: with mock.patch( - "google.auth.transport.grpc.SslCredentials.__init__", return_value=None + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, ): with mock.patch( - "google.auth.transport.grpc.SslCredentials.is_mtls", - new_callable=mock.PropertyMock, - ) as is_mtls_mock: - with mock.patch( - "google.auth.transport.grpc.SslCredentials.ssl_credentials", - new_callable=mock.PropertyMock, - ) as ssl_credentials_mock: - if use_client_cert_env == "false": - is_mtls_mock.return_value = False - ssl_credentials_mock.return_value = None - expected_host = client.DEFAULT_ENDPOINT - expected_ssl_channel_creds = None - else: - is_mtls_mock.return_value = True - ssl_credentials_mock.return_value = mock.Mock() - expected_host = client.DEFAULT_MTLS_ENDPOINT - expected_ssl_channel_creds = ( - ssl_credentials_mock.return_value - ) - - patched.return_value = None - client = client_class() - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=expected_host, - scopes=None, - ssl_channel_credentials=expected_ssl_channel_creds, - quota_project_id=None, - client_info=transports.base.DEFAULT_CLIENT_INFO, - ) + "google.auth.transport.mtls.default_client_cert_source", + return_value=client_cert_source_callback, + ): + if use_client_cert_env == "false": + expected_host = client.DEFAULT_ENDPOINT + expected_client_cert_source = None + else: + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_client_cert_source = client_cert_source_callback - # Check the case client_cert_source and ADC client cert are not provided. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} - ): - with mock.patch.object(transport_class, "__init__") as patched: - with mock.patch( - "google.auth.transport.grpc.SslCredentials.__init__", return_value=None - ): - with mock.patch( - "google.auth.transport.grpc.SslCredentials.is_mtls", - new_callable=mock.PropertyMock, - ) as is_mtls_mock: - is_mtls_mock.return_value = False patched.return_value = None client = client_class() patched.assert_called_once_with( credentials=None, credentials_file=None, - host=client.DEFAULT_ENDPOINT, + host=expected_host, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=expected_client_cert_source, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) + # Check the case client_cert_source and ADC client cert are not provided. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + @pytest.mark.parametrize( "client_class,transport_class,transport_name", @@ -378,7 +381,7 @@ def test_cloud_billing_client_client_options_scopes( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=["1", "2"], - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -408,7 +411,7 @@ def test_cloud_billing_client_client_options_credentials_file( credentials_file="credentials.json", host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -425,7 +428,7 @@ def test_cloud_billing_client_client_options_from_dict(): credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -479,6 +482,24 @@ def test_get_billing_account_from_dict(): test_get_billing_account(request_type=dict) +def test_get_billing_account_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_billing_account), "__call__" + ) as call: + client.get_billing_account() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_billing.GetBillingAccountRequest() + + @pytest.mark.asyncio async def test_get_billing_account_async( transport: str = "grpc_asyncio", request_type=cloud_billing.GetBillingAccountRequest @@ -695,6 +716,24 @@ def test_list_billing_accounts_from_dict(): test_list_billing_accounts(request_type=dict) +def test_list_billing_accounts_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.list_billing_accounts), "__call__" + ) as call: + client.list_billing_accounts() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_billing.ListBillingAccountsRequest() + + @pytest.mark.asyncio async def test_list_billing_accounts_async( transport: str = "grpc_asyncio", @@ -953,6 +992,24 @@ def test_update_billing_account_from_dict(): test_update_billing_account(request_type=dict) +def test_update_billing_account_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_billing_account), "__call__" + ) as call: + client.update_billing_account() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_billing.UpdateBillingAccountRequest() + + @pytest.mark.asyncio async def test_update_billing_account_async( transport: str = "grpc_asyncio", @@ -1191,6 +1248,24 @@ def test_create_billing_account_from_dict(): test_create_billing_account(request_type=dict) +def test_create_billing_account_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.create_billing_account), "__call__" + ) as call: + client.create_billing_account() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_billing.CreateBillingAccountRequest() + + @pytest.mark.asyncio async def test_create_billing_account_async( transport: str = "grpc_asyncio", @@ -1363,6 +1438,24 @@ def test_list_project_billing_info_from_dict(): test_list_project_billing_info(request_type=dict) +def test_list_project_billing_info_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.list_project_billing_info), "__call__" + ) as call: + client.list_project_billing_info() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_billing.ListProjectBillingInfoRequest() + + @pytest.mark.asyncio async def test_list_project_billing_info_async( transport: str = "grpc_asyncio", @@ -1750,6 +1843,24 @@ def test_get_project_billing_info_from_dict(): test_get_project_billing_info(request_type=dict) +def test_get_project_billing_info_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_project_billing_info), "__call__" + ) as call: + client.get_project_billing_info() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_billing.GetProjectBillingInfoRequest() + + @pytest.mark.asyncio async def test_get_project_billing_info_async( transport: str = "grpc_asyncio", @@ -1976,6 +2087,24 @@ def test_update_project_billing_info_from_dict(): test_update_project_billing_info(request_type=dict) +def test_update_project_billing_info_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_project_billing_info), "__call__" + ) as call: + client.update_project_billing_info() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_billing.UpdateProjectBillingInfoRequest() + + @pytest.mark.asyncio async def test_update_project_billing_info_async( transport: str = "grpc_asyncio", @@ -2209,6 +2338,22 @@ def test_get_iam_policy_from_dict(): test_get_iam_policy(request_type=dict) +def test_get_iam_policy_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_iam_policy), "__call__") as call: + client.get_iam_policy() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == iam_policy.GetIamPolicyRequest() + + @pytest.mark.asyncio async def test_get_iam_policy_async( transport: str = "grpc_asyncio", request_type=iam_policy.GetIamPolicyRequest @@ -2416,6 +2561,22 @@ def test_set_iam_policy_from_dict(): test_set_iam_policy(request_type=dict) +def test_set_iam_policy_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_iam_policy), "__call__") as call: + client.set_iam_policy() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == iam_policy.SetIamPolicyRequest() + + @pytest.mark.asyncio async def test_set_iam_policy_async( transport: str = "grpc_asyncio", request_type=iam_policy.SetIamPolicyRequest @@ -2625,6 +2786,24 @@ def test_test_iam_permissions_from_dict(): test_test_iam_permissions(request_type=dict) +def test_test_iam_permissions_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudBillingClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.test_iam_permissions), "__call__" + ) as call: + client.test_iam_permissions() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == iam_policy.TestIamPermissionsRequest() + + @pytest.mark.asyncio async def test_test_iam_permissions_async( transport: str = "grpc_asyncio", request_type=iam_policy.TestIamPermissionsRequest @@ -2877,7 +3056,10 @@ def test_transport_get_channel(): @pytest.mark.parametrize( "transport_class", - [transports.CloudBillingGrpcTransport, transports.CloudBillingGrpcAsyncIOTransport], + [ + transports.CloudBillingGrpcTransport, + transports.CloudBillingGrpcAsyncIOTransport, + ], ) def test_transport_adc(transport_class): # Test default credentials are used if not provided. @@ -2986,6 +3168,48 @@ def test_cloud_billing_transport_auth_adc(): ) +@pytest.mark.parametrize( + "transport_class", + [transports.CloudBillingGrpcTransport, transports.CloudBillingGrpcAsyncIOTransport], +) +def test_cloud_billing_grpc_transport_client_cert_source_for_mtls(transport_class): + cred = credentials.AnonymousCredentials() + + # Check ssl_channel_credentials is used if provided. + with mock.patch.object(transport_class, "create_channel") as mock_create_channel: + mock_ssl_channel_creds = mock.Mock() + transport_class( + host="squid.clam.whelk", + credentials=cred, + ssl_channel_credentials=mock_ssl_channel_creds, + ) + mock_create_channel.assert_called_once_with( + "squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ssl_credentials=mock_ssl_channel_creds, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Check if ssl_channel_credentials is not provided, then client_cert_source_for_mtls + # is used. + with mock.patch.object(transport_class, "create_channel", return_value=mock.Mock()): + with mock.patch("grpc.ssl_channel_credentials") as mock_ssl_cred: + transport_class( + credentials=cred, + client_cert_source_for_mtls=client_cert_source_callback, + ) + expected_cert, expected_key = client_cert_source_callback() + mock_ssl_cred.assert_called_once_with( + certificate_chain=expected_cert, private_key=expected_key + ) + + def test_cloud_billing_host_no_port(): client = CloudBillingClient( credentials=credentials.AnonymousCredentials(), @@ -3007,7 +3231,7 @@ def test_cloud_billing_host_with_port(): def test_cloud_billing_grpc_transport_channel(): - channel = grpc.insecure_channel("http://localhost/") + channel = grpc.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.CloudBillingGrpcTransport( @@ -3019,7 +3243,7 @@ def test_cloud_billing_grpc_transport_channel(): def test_cloud_billing_grpc_asyncio_transport_channel(): - channel = aio.insecure_channel("http://localhost/") + channel = aio.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.CloudBillingGrpcAsyncIOTransport( @@ -3030,6 +3254,8 @@ def test_cloud_billing_grpc_asyncio_transport_channel(): assert transport._ssl_channel_credentials == None +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [transports.CloudBillingGrpcTransport, transports.CloudBillingGrpcAsyncIOTransport], @@ -3039,7 +3265,7 @@ def test_cloud_billing_transport_channel_mtls_with_client_cert_source(transport_ "grpc.ssl_channel_credentials", autospec=True ) as grpc_ssl_channel_cred: with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_ssl_cred = mock.Mock() grpc_ssl_channel_cred.return_value = mock_ssl_cred @@ -3077,6 +3303,8 @@ def test_cloud_billing_transport_channel_mtls_with_client_cert_source(transport_ assert transport._ssl_channel_credentials == mock_ssl_cred +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [transports.CloudBillingGrpcTransport, transports.CloudBillingGrpcAsyncIOTransport], @@ -3089,7 +3317,7 @@ def test_cloud_billing_transport_channel_mtls_with_adc(transport_class): ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), ): with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_grpc_channel = mock.Mock() grpc_create_channel.return_value = mock_grpc_channel diff --git a/tests/unit/gapic/billing_v1/test_cloud_catalog.py b/tests/unit/gapic/billing_v1/test_cloud_catalog.py index 0ac2a31..26d0354 100644 --- a/tests/unit/gapic/billing_v1/test_cloud_catalog.py +++ b/tests/unit/gapic/billing_v1/test_cloud_catalog.py @@ -82,7 +82,22 @@ def test__get_default_mtls_endpoint(): assert CloudCatalogClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi -@pytest.mark.parametrize("client_class", [CloudCatalogClient, CloudCatalogAsyncClient]) +@pytest.mark.parametrize("client_class", [CloudCatalogClient, CloudCatalogAsyncClient,]) +def test_cloud_catalog_client_from_service_account_info(client_class): + creds = credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = client_class.from_service_account_info(info) + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "cloudbilling.googleapis.com:443" + + +@pytest.mark.parametrize("client_class", [CloudCatalogClient, CloudCatalogAsyncClient,]) def test_cloud_catalog_client_from_service_account_file(client_class): creds = credentials.AnonymousCredentials() with mock.patch.object( @@ -91,16 +106,21 @@ def test_cloud_catalog_client_from_service_account_file(client_class): factory.return_value = creds client = client_class.from_service_account_file("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) client = client_class.from_service_account_json("dummy/file/path.json") assert client.transport._credentials == creds + assert isinstance(client, client_class) assert client.transport._host == "cloudbilling.googleapis.com:443" def test_cloud_catalog_client_get_transport_class(): transport = CloudCatalogClient.get_transport_class() - assert transport == transports.CloudCatalogGrpcTransport + available_transports = [ + transports.CloudCatalogGrpcTransport, + ] + assert transport in available_transports transport = CloudCatalogClient.get_transport_class("grpc") assert transport == transports.CloudCatalogGrpcTransport @@ -149,7 +169,7 @@ def test_cloud_catalog_client_client_options( credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -165,7 +185,7 @@ def test_cloud_catalog_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -181,7 +201,7 @@ def test_cloud_catalog_client_client_options( credentials_file=None, host=client.DEFAULT_MTLS_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -209,7 +229,7 @@ def test_cloud_catalog_client_client_options( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id="octopus", client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -258,29 +278,25 @@ def test_cloud_catalog_client_mtls_env_auto( client_cert_source=client_cert_source_callback ) with mock.patch.object(transport_class, "__init__") as patched: - ssl_channel_creds = mock.Mock() - with mock.patch( - "grpc.ssl_channel_credentials", return_value=ssl_channel_creds - ): - patched.return_value = None - client = client_class(client_options=options) + patched.return_value = None + client = client_class(client_options=options) - if use_client_cert_env == "false": - expected_ssl_channel_creds = None - expected_host = client.DEFAULT_ENDPOINT - else: - expected_ssl_channel_creds = ssl_channel_creds - expected_host = client.DEFAULT_MTLS_ENDPOINT + if use_client_cert_env == "false": + expected_client_cert_source = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_client_cert_source = client_cert_source_callback + expected_host = client.DEFAULT_MTLS_ENDPOINT - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=expected_host, - scopes=None, - ssl_channel_credentials=expected_ssl_channel_creds, - quota_project_id=None, - client_info=transports.base.DEFAULT_CLIENT_INFO, - ) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) # Check the case ADC client cert is provided. Whether client cert is used depends on # GOOGLE_API_USE_CLIENT_CERTIFICATE value. @@ -289,66 +305,53 @@ def test_cloud_catalog_client_mtls_env_auto( ): with mock.patch.object(transport_class, "__init__") as patched: with mock.patch( - "google.auth.transport.grpc.SslCredentials.__init__", return_value=None + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, ): with mock.patch( - "google.auth.transport.grpc.SslCredentials.is_mtls", - new_callable=mock.PropertyMock, - ) as is_mtls_mock: - with mock.patch( - "google.auth.transport.grpc.SslCredentials.ssl_credentials", - new_callable=mock.PropertyMock, - ) as ssl_credentials_mock: - if use_client_cert_env == "false": - is_mtls_mock.return_value = False - ssl_credentials_mock.return_value = None - expected_host = client.DEFAULT_ENDPOINT - expected_ssl_channel_creds = None - else: - is_mtls_mock.return_value = True - ssl_credentials_mock.return_value = mock.Mock() - expected_host = client.DEFAULT_MTLS_ENDPOINT - expected_ssl_channel_creds = ( - ssl_credentials_mock.return_value - ) - - patched.return_value = None - client = client_class() - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=expected_host, - scopes=None, - ssl_channel_credentials=expected_ssl_channel_creds, - quota_project_id=None, - client_info=transports.base.DEFAULT_CLIENT_INFO, - ) + "google.auth.transport.mtls.default_client_cert_source", + return_value=client_cert_source_callback, + ): + if use_client_cert_env == "false": + expected_host = client.DEFAULT_ENDPOINT + expected_client_cert_source = None + else: + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_client_cert_source = client_cert_source_callback - # Check the case client_cert_source and ADC client cert are not provided. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} - ): - with mock.patch.object(transport_class, "__init__") as patched: - with mock.patch( - "google.auth.transport.grpc.SslCredentials.__init__", return_value=None - ): - with mock.patch( - "google.auth.transport.grpc.SslCredentials.is_mtls", - new_callable=mock.PropertyMock, - ) as is_mtls_mock: - is_mtls_mock.return_value = False patched.return_value = None client = client_class() patched.assert_called_once_with( credentials=None, credentials_file=None, - host=client.DEFAULT_ENDPOINT, + host=expected_host, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=expected_client_cert_source, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) + # Check the case client_cert_source and ADC client cert are not provided. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + ) + @pytest.mark.parametrize( "client_class,transport_class,transport_name", @@ -374,7 +377,7 @@ def test_cloud_catalog_client_client_options_scopes( credentials_file=None, host=client.DEFAULT_ENDPOINT, scopes=["1", "2"], - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -404,7 +407,7 @@ def test_cloud_catalog_client_client_options_credentials_file( credentials_file="credentials.json", host=client.DEFAULT_ENDPOINT, scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -421,7 +424,7 @@ def test_cloud_catalog_client_client_options_from_dict(): credentials_file=None, host="squid.clam.whelk", scopes=None, - ssl_channel_credentials=None, + client_cert_source_for_mtls=None, quota_project_id=None, client_info=transports.base.DEFAULT_CLIENT_INFO, ) @@ -464,6 +467,22 @@ def test_list_services_from_dict(): test_list_services(request_type=dict) +def test_list_services_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudCatalogClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_services), "__call__") as call: + client.list_services() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_catalog.ListServicesRequest() + + @pytest.mark.asyncio async def test_list_services_async( transport: str = "grpc_asyncio", request_type=cloud_catalog.ListServicesRequest @@ -672,6 +691,22 @@ def test_list_skus_from_dict(): test_list_skus(request_type=dict) +def test_list_skus_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CloudCatalogClient( + credentials=credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_skus), "__call__") as call: + client.list_skus() + call.assert_called() + _, args, _ = call.mock_calls[0] + + assert args[0] == cloud_catalog.ListSkusRequest() + + @pytest.mark.asyncio async def test_list_skus_async( transport: str = "grpc_asyncio", request_type=cloud_catalog.ListSkusRequest @@ -1006,7 +1041,10 @@ def test_transport_get_channel(): @pytest.mark.parametrize( "transport_class", - [transports.CloudCatalogGrpcTransport, transports.CloudCatalogGrpcAsyncIOTransport], + [ + transports.CloudCatalogGrpcTransport, + transports.CloudCatalogGrpcAsyncIOTransport, + ], ) def test_transport_adc(transport_class): # Test default credentials are used if not provided. @@ -1107,6 +1145,48 @@ def test_cloud_catalog_transport_auth_adc(): ) +@pytest.mark.parametrize( + "transport_class", + [transports.CloudCatalogGrpcTransport, transports.CloudCatalogGrpcAsyncIOTransport], +) +def test_cloud_catalog_grpc_transport_client_cert_source_for_mtls(transport_class): + cred = credentials.AnonymousCredentials() + + # Check ssl_channel_credentials is used if provided. + with mock.patch.object(transport_class, "create_channel") as mock_create_channel: + mock_ssl_channel_creds = mock.Mock() + transport_class( + host="squid.clam.whelk", + credentials=cred, + ssl_channel_credentials=mock_ssl_channel_creds, + ) + mock_create_channel.assert_called_once_with( + "squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ssl_credentials=mock_ssl_channel_creds, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Check if ssl_channel_credentials is not provided, then client_cert_source_for_mtls + # is used. + with mock.patch.object(transport_class, "create_channel", return_value=mock.Mock()): + with mock.patch("grpc.ssl_channel_credentials") as mock_ssl_cred: + transport_class( + credentials=cred, + client_cert_source_for_mtls=client_cert_source_callback, + ) + expected_cert, expected_key = client_cert_source_callback() + mock_ssl_cred.assert_called_once_with( + certificate_chain=expected_cert, private_key=expected_key + ) + + def test_cloud_catalog_host_no_port(): client = CloudCatalogClient( credentials=credentials.AnonymousCredentials(), @@ -1128,7 +1208,7 @@ def test_cloud_catalog_host_with_port(): def test_cloud_catalog_grpc_transport_channel(): - channel = grpc.insecure_channel("http://localhost/") + channel = grpc.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.CloudCatalogGrpcTransport( @@ -1140,7 +1220,7 @@ def test_cloud_catalog_grpc_transport_channel(): def test_cloud_catalog_grpc_asyncio_transport_channel(): - channel = aio.insecure_channel("http://localhost/") + channel = aio.secure_channel("http://localhost/", grpc.local_channel_credentials()) # Check that channel is used if provided. transport = transports.CloudCatalogGrpcAsyncIOTransport( @@ -1151,6 +1231,8 @@ def test_cloud_catalog_grpc_asyncio_transport_channel(): assert transport._ssl_channel_credentials == None +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [transports.CloudCatalogGrpcTransport, transports.CloudCatalogGrpcAsyncIOTransport], @@ -1160,7 +1242,7 @@ def test_cloud_catalog_transport_channel_mtls_with_client_cert_source(transport_ "grpc.ssl_channel_credentials", autospec=True ) as grpc_ssl_channel_cred: with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_ssl_cred = mock.Mock() grpc_ssl_channel_cred.return_value = mock_ssl_cred @@ -1198,6 +1280,8 @@ def test_cloud_catalog_transport_channel_mtls_with_client_cert_source(transport_ assert transport._ssl_channel_credentials == mock_ssl_cred +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. @pytest.mark.parametrize( "transport_class", [transports.CloudCatalogGrpcTransport, transports.CloudCatalogGrpcAsyncIOTransport], @@ -1210,7 +1294,7 @@ def test_cloud_catalog_transport_channel_mtls_with_adc(transport_class): ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), ): with mock.patch.object( - transport_class, "create_channel", autospec=True + transport_class, "create_channel" ) as grpc_create_channel: mock_grpc_channel = mock.Mock() grpc_create_channel.return_value = mock_grpc_channel