diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index cb89b2e32..480226ac0 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,16 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:ec49167c606648a063d1222220b48119c912562849a0528f35bfb592a9f72737 + digest: sha256:6162c384d685c5fe22521d3f37f6fc732bf99a085f6d47b677dbcae97fc21392 diff --git a/.github/release-please.yml b/.github/release-please.yml index 4507ad059..466597e5b 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -1 +1,2 @@ releaseType: python +handleGHRelease: true diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml new file mode 100644 index 000000000..d4ca94189 --- /dev/null +++ b/.github/release-trigger.yml @@ -0,0 +1 @@ +enabled: true diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 2f68b5a4d..48639e7e7 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -11,6 +11,5 @@ branchProtectionRules: - 'Kokoro - Against Pub/Sub Lite samples' - 'cla/google' - 'Samples - Lint' - - 'Samples - Python 3.6' - 'Samples - Python 3.7' - 'Samples - Python 3.8' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..b46d7305d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,38 @@ +on: + pull_request: + branches: + - main +name: docs +jobs: + docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run docs + run: | + nox -s docs + docfx: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run docfx + run: | + nox -s docfx diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..f512a4960 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +on: + pull_request: + branches: + - main +name: lint +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run lint + run: | + nox -s lint + - name: Run lint_setup_py + run: | + nox -s lint_setup_py diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 000000000..e87fe5b7b --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,57 @@ +on: + pull_request: + branches: + - main +name: unittest +jobs: + unit: + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.6', '3.7', '3.8', '3.9', '3.10'] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python }} + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run unit tests + env: + COVERAGE_FILE: .coverage-${{ matrix.python }} + run: | + nox -s unit-${{ matrix.python }} + - name: Upload coverage results + uses: actions/upload-artifact@v2 + with: + name: coverage-artifacts + path: .coverage-${{ matrix.python }} + + cover: + runs-on: ubuntu-latest + needs: + - unit + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install coverage + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install coverage + - name: Download coverage results + uses: actions/download-artifact@v2 + with: + name: coverage-artifacts + path: .coverage-results/ + - name: Report coverage results + run: | + coverage combine .coverage-results/.coverage* + coverage report --show-missing --fail-under=100 diff --git a/.gitignore b/.gitignore index 99c3a1444..b4243ced7 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ pip-log.txt .nox .cache .pytest_cache -.pytype # Mac diff --git a/.kokoro/presubmit-against-pubsublite-samples.sh b/.kokoro/presubmit-against-pubsublite-samples.sh index a93980bc3..ff143a394 100755 --- a/.kokoro/presubmit-against-pubsublite-samples.sh +++ b/.kokoro/presubmit-against-pubsublite-samples.sh @@ -79,6 +79,7 @@ for file in python-pubsublite/samples/**/requirements.txt; do python3.6 -m venv py-3.6 source py-3.6/bin/activate # Install python-pubsublite samples tests requirements. + python -m pip install --upgrade pip python -m pip install -r requirements.txt -q python -m pip install -r requirements-test.txt -q # Install python-pubsub from source. diff --git a/.kokoro/release.sh b/.kokoro/release.sh index b3a2d20a8..5c00cba3e 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -26,7 +26,7 @@ python3 -m pip install --upgrade twine wheel setuptools export PYTHONUNBUFFERED=1 # Move into the package, build the distribution and upload. -TWINE_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secret_manager/google-cloud-pypi-token") +TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-1") cd github/python-pubsub python3 setup.py sdist bdist_wheel twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index 1648dd9ad..c67fccae4 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -23,8 +23,18 @@ env_vars: { value: "github/python-pubsub/.kokoro/release.sh" } +# Fetch PyPI password +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "google-cloud-pypi-token-keystore-1" + } + } +} + # Tokens needed to report release status back to GitHub env_vars: { key: "SECRET_MANAGER_KEYS" - value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem,google-cloud-pypi-token" + value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" } diff --git a/.repo-metadata.json b/.repo-metadata.json index fa07857c2..21bdab122 100644 --- a/.repo-metadata.json +++ b/.repo-metadata.json @@ -2,14 +2,16 @@ "name": "pubsub", "name_pretty": "Google Cloud Pub/Sub", "product_documentation": "https://cloud.google.com/pubsub/docs/", - "client_documentation": "https://googleapis.dev/python/pubsub/latest", + "client_documentation": "https://cloud.google.com/python/docs/reference/pubsub/latest", "issue_tracker": "https://issuetracker.google.com/savedsearches/559741", - "release_level": "ga", + "release_level": "stable", "language": "python", "repo": "googleapis/python-pubsub", "distribution_name": "google-cloud-pubsub", "api_id": "pubsub.googleapis.com", "requires_billing": true, "default_version": "v1", - "codeowner_team": "@googleapis/api-pubsub" + "codeowner_team": "@googleapis/api-pubsub", + "api_shortname": "pubsub", + "library_type": "GAPIC_COMBO" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 00a8aae50..57732bacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ [1]: https://pypi.org/project/google-cloud-pubsub/#history +## [2.10.0](https://github.com/googleapis/python-pubsub/compare/v2.9.0...v2.10.0) (2022-03-04) + + +### Features + +* add api key support ([#571](https://github.com/googleapis/python-pubsub/issues/571)) ([cdda762](https://github.com/googleapis/python-pubsub/commit/cdda762f6d15d96f5e2d7fac975f3494dc49eaa9)) +* add exactly once delivery flag ([#577](https://github.com/googleapis/python-pubsub/issues/577)) ([d6614e2](https://github.com/googleapis/python-pubsub/commit/d6614e274328c58449e67dfc788e2e7986c0c10b)) +* add support for exactly once delivery ([#578](https://github.com/googleapis/python-pubsub/issues/578)) ([95a86fa](https://github.com/googleapis/python-pubsub/commit/95a86fa5f528701b760064f0cece0efa4e60cd44)) +* exactly-once delivery support ([#550](https://github.com/googleapis/python-pubsub/issues/550)) ([2fb6e15](https://github.com/googleapis/python-pubsub/commit/2fb6e1533192ae81dceee5c71283169a0a85a015)) + + +### Bug Fixes + +* **deps:** move libcst to extras ([#585](https://github.com/googleapis/python-pubsub/issues/585)) ([0846762](https://github.com/googleapis/python-pubsub/commit/084676243ca4afd54cda601e589b80883f9703a3)) +* refactor client classes for safer type checking ([#552](https://github.com/googleapis/python-pubsub/issues/552)) ([7f705be](https://github.com/googleapis/python-pubsub/commit/7f705beb927383f14b9d56f0341ee0de101f7c05)) +* resolve DuplicateCredentialArgs error when using credentials_file ([8ca8cf2](https://github.com/googleapis/python-pubsub/commit/8ca8cf27333baf823a1dffd081e63079f1a12625)) + + +### Samples +* samples: create subscription with filtering enabled [#580](https://github.com/googleapis/python-pubsub/pull/580) +* samples: handle empty response in sync pull samples [#586](https://github.com/googleapis/python-pubsub/pull/586) +* samples: sample for receiving messages with exactly-once delivery enabled [#588](https://github.com/googleapis/python-pubsub/pull/588) +* samples: create subscription with exactly once delivery [#592](https://github.com/googleapis/python-pubsub/pull/592) +(https://github.com/googleapis/python-pubsub/pull/588 + + +### Documentation + +* add autogenerated code snippets ([aa3754c](https://github.com/googleapis/python-pubsub/commit/aa3754cf432bd02be2734a23a32d5b36cd216aee)) +* Docs have inconsistent default values for max_latency and max_bytes ([#572](https://github.com/googleapis/python-pubsub/issues/572)) ([d136dfd](https://github.com/googleapis/python-pubsub/commit/d136dfdb69ebeebd1411a1415f863b94d07078f0)) + ## [2.9.0](https://www.github.com/googleapis/python-pubsub/compare/v2.8.0...v2.9.0) (2021-11-10) diff --git a/README.rst b/README.rst index 7d671c42b..6432525b1 100644 --- a/README.rst +++ b/README.rst @@ -27,7 +27,7 @@ independently written applications. :target: https://pypi.org/project/google-cloud-pubsub/ .. _Google Cloud Pub / Sub: https://cloud.google.com/pubsub/ .. _Product Documentation: https://cloud.google.com/pubsub/docs -.. _Client Library Documentation: https://googleapis.dev/python/pubsub/latest +.. _Client Library Documentation: https://cloud.google.com/python/docs/reference/pubsub/latest Quick Start ----------- @@ -116,7 +116,7 @@ messages to it To learn more, consult the `publishing documentation`_. -.. _publishing documentation: https://googleapis.dev/python/pubsub/latest +.. _publishing documentation: https://cloud.google.com/python/docs/reference/pubsub/latest Subscribing @@ -162,7 +162,7 @@ block the current thread until a given condition obtains: It is also possible to pull messages in a synchronous (blocking) fashion. To learn more about subscribing, consult the `subscriber documentation`_. -.. _subscriber documentation: https://googleapis.dev/python/pubsub/latest +.. _subscriber documentation: https://cloud.google.com/python/docs/reference/pubsub/latest Authentication diff --git a/UPGRADING.md b/UPGRADING.md index 3033e3fd4..777dd7ae6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -24,10 +24,10 @@ The 2.0.0 release requires Python 3.6+. Almost all methods that send requests to the backend expect request objects. We provide a script that will convert most common use cases. -* Install the library +* Install the library with the `libcst` extra. ```py -python3 -m pip install google-cloud-pubsub +python3 -m pip install google-cloud-pubsub[libcst] ``` * The script `fixup_pubsub_v1_keywords.py` is shipped with the library. It expects diff --git a/docs/publisher/index.rst b/docs/publisher/index.rst index 0e7a9b50b..2a0ad320e 100644 --- a/docs/publisher/index.rst +++ b/docs/publisher/index.rst @@ -72,7 +72,7 @@ The way that this works is that on the first message that you send, a new batch is created automatically. For every subsequent message, if there is already a valid batch that is still accepting messages, then that batch is used. When the batch is created, it begins a countdown that publishes the batch once -sufficient time has elapsed (by default, this is 0.05 seconds). +sufficient time has elapsed (by default, this is 0.01 seconds). If you need different batching settings, simply provide a :class:`~.pubsub_v1.types.BatchSettings` object when you instantiate the @@ -84,11 +84,23 @@ If you need different batching settings, simply provide a from google.cloud.pubsub import types client = pubsub.PublisherClient( - batch_settings=types.BatchSettings(max_messages=500), + batch_settings=types.BatchSettings( + max_messages=500, # default 100 + max_bytes=1024, # default 1 MB + max_latency=1 # default .01 seconds + ), ) -Pub/Sub accepts a maximum of 1,000 messages in a batch, and the size of a -batch can not exceed 10 megabytes. +The `max_bytes` argument is the maximum total size of the messages to collect +before automatically publishing the batch, (in bytes) including any byte size +overhead of the publish request itself. The maximum value is bound by the +server-side limit of 10_000_000 bytes. The default value is 1 MB. + +The `max_messages` argument is the maximum number of messages to collect +before automatically publishing the batch, the default value is 100 messages. + +The `max_latency` is the maximum number of seconds to wait for additional +messages before automatically publishing the batch, the default is .01 seconds. Futures diff --git a/google/cloud/__init__.py b/google/cloud/__init__.py index 9a1b64a6d..e1f8a4d20 100644 --- a/google/cloud/__init__.py +++ b/google/cloud/__init__.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List + try: import pkg_resources @@ -21,4 +23,4 @@ except ImportError: import pkgutil - __path__ = pkgutil.extend_path(__path__, __name__) + __path__: List[str] = pkgutil.extend_path(__path__, __name__) # type: ignore diff --git a/google/cloud/pubsub_v1/_gapic.py b/google/cloud/pubsub_v1/_gapic.py deleted file mode 100644 index e25c1dc6c..000000000 --- a/google/cloud/pubsub_v1/_gapic.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2019, Google LLC All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import - -import functools -from typing import Callable, Container, Type - - -def add_methods( - source_class: Type, denylist: Container[str] = () -) -> Callable[[Type], Type]: - """Add wrapped versions of the `api` member's methods to the class. - - Any methods passed in `denylist` are not added. - Additionally, any methods explicitly defined on the wrapped class are - not added. - """ - - def wrap(wrapped_fx: Callable, lookup_fx: Callable): - """Wrap a GAPIC method; preserve its name and docstring.""" - # If this is a static or class method, then we do *not* - # send self as the first argument. - # - # For instance methods, we need to send self.api rather - # than self, since that is where the actual methods were declared. - - if isinstance(lookup_fx, (classmethod, staticmethod)): - fx = lambda *a, **kw: wrapped_fx(*a, **kw) # noqa - return staticmethod(functools.wraps(wrapped_fx)(fx)) - else: - fx = lambda self, *a, **kw: wrapped_fx(self.api, *a, **kw) # noqa - return functools.wraps(wrapped_fx)(fx) - - def actual_decorator(cls: Type) -> Type: - # Reflectively iterate over most of the methods on the source class - # (the GAPIC) and make wrapped versions available on this client. - for name in dir(source_class): - # Ignore all private and magic methods. - if name.startswith("_"): - continue - - # Ignore anything on our denylist. - if name in denylist: - continue - - # Retrieve the attribute, and ignore it if it is not callable. - attr = getattr(source_class, name) - if not callable(attr): - continue - - # Add a wrapper method to this object. - lookup_fx = source_class.__dict__[name] - fx = wrap(attr, lookup_fx) - - setattr(cls, name, fx) - - # Return the augmented class. - return cls - - # Simply return the actual decorator; this is returned from this method - # and actually used to decorate the class. - return actual_decorator diff --git a/google/cloud/pubsub_v1/futures.py b/google/cloud/pubsub_v1/futures.py index d8acc8ea5..5527d21d0 100644 --- a/google/cloud/pubsub_v1/futures.py +++ b/google/cloud/pubsub_v1/futures.py @@ -15,7 +15,7 @@ from __future__ import absolute_import import concurrent.futures -from typing import Any, NoReturn +from typing import Any, NoReturn, Optional import google.api_core.future @@ -47,7 +47,7 @@ def set_result(self, result: Any): """ return super().set_result(result=result) - def set_exception(self, exception: Exception): + def set_exception(self, exception: Optional[BaseException]): """Set the result of the future as being the given exception. Do not use this method, it should only be used internally by the library and its diff --git a/google/cloud/pubsub_v1/proto/pubsub.proto b/google/cloud/pubsub_v1/proto/pubsub.proto index c5cb855d6..716c7ba05 100644 --- a/google/cloud/pubsub_v1/proto/pubsub.proto +++ b/google/cloud/pubsub_v1/proto/pubsub.proto @@ -1164,6 +1164,7 @@ message StreamingPullRequest { message StreamingPullResponse { // Subscription properties sent as part of the response. message SubscriptionProperties { + bool exactly_once_delivery_enabled = 1; // True iff message ordering is enabled for this subscription. bool message_ordering_enabled = 2; } diff --git a/google/cloud/pubsub_v1/publisher/_batch/base.py b/google/cloud/pubsub_v1/publisher/_batch/base.py index f32028360..52505996b 100644 --- a/google/cloud/pubsub_v1/publisher/_batch/base.py +++ b/google/cloud/pubsub_v1/publisher/_batch/base.py @@ -58,7 +58,7 @@ def __len__(self): @staticmethod @abc.abstractmethod - def make_lock() -> None: # pragma: NO COVER + def make_lock(): # pragma: NO COVER """Return a lock in the chosen concurrency model. Returns: diff --git a/google/cloud/pubsub_v1/publisher/_batch/thread.py b/google/cloud/pubsub_v1/publisher/_batch/thread.py index d68d00a0e..8b868eaee 100644 --- a/google/cloud/pubsub_v1/publisher/_batch/thread.py +++ b/google/cloud/pubsub_v1/publisher/_batch/thread.py @@ -18,7 +18,7 @@ import threading import time import typing -from typing import Any, Callable, Optional, Sequence +from typing import Any, Callable, List, Optional, Sequence import google.api_core.exceptions from google.api_core import gapic_v1 @@ -28,11 +28,10 @@ from google.pubsub_v1 import types as gapic_types if typing.TYPE_CHECKING: # pragma: NO COVER - from google import api_core from google.cloud import pubsub_v1 from google.cloud.pubsub_v1 import types - from google.cloud.pubsub_v1 import PublisherClient - + from google.cloud.pubsub_v1.publisher import Client as PublisherClient + from google.pubsub_v1.services.publisher.client import OptionalRetry _LOGGER = logging.getLogger(__name__) _CAN_COMMIT = (base.BatchStatus.ACCEPTING_MESSAGES, base.BatchStatus.STARTING) @@ -93,8 +92,8 @@ def __init__( settings: "types.BatchSettings", batch_done_callback: Callable[[bool], Any] = None, commit_when_full: bool = True, - commit_retry: "api_core.retry.Retry" = gapic_v1.method.DEFAULT, - commit_timeout: gapic_types.TimeoutType = gapic_v1.method.DEFAULT, + commit_retry: "OptionalRetry" = gapic_v1.method.DEFAULT, + commit_timeout: "types.OptionalTimeout" = gapic_v1.method.DEFAULT, ): self._client = client self._topic = topic @@ -108,8 +107,8 @@ def __init__( # _futures list should remain unchanged after batch # status changed from ACCEPTING_MESSAGES to any other # in order to avoid race conditions - self._futures = [] - self._messages = [] + self._futures: List[futures.Future] = [] + self._messages: List[gapic_types.PubsubMessage] = [] self._status = base.BatchStatus.ACCEPTING_MESSAGES # The initial size is not zero, we need to account for the size overhead @@ -272,7 +271,7 @@ def _commit(self) -> None: batch_transport_succeeded = True try: # Performs retries for errors defined by the retry configuration. - response = self._client.api.publish( + response = self._client._gapic_publish( topic=self._topic, messages=self._messages, retry=self._commit_retry, @@ -368,7 +367,7 @@ def publish( ), "Publish after stop() or publish error." if self.status != base.BatchStatus.ACCEPTING_MESSAGES: - return + return None size_increase = gapic_types.PublishRequest( messages=[message] diff --git a/google/cloud/pubsub_v1/publisher/_sequencer/base.py b/google/cloud/pubsub_v1/publisher/_sequencer/base.py index 49bdcb740..7a0c28e45 100644 --- a/google/cloud/pubsub_v1/publisher/_sequencer/base.py +++ b/google/cloud/pubsub_v1/publisher/_sequencer/base.py @@ -22,7 +22,7 @@ if typing.TYPE_CHECKING: # pragma: NO COVER from concurrent import futures - from google.api_core import retry + from google.pubsub_v1.services.publisher.client import OptionalRetry class Sequencer(metaclass=abc.ABCMeta): @@ -30,7 +30,6 @@ class Sequencer(metaclass=abc.ABCMeta): sequences messages to be published. """ - @staticmethod @abc.abstractmethod def is_finished(self) -> bool: # pragma: NO COVER """ Whether the sequencer is finished and should be cleaned up. @@ -40,7 +39,6 @@ def is_finished(self) -> bool: # pragma: NO COVER """ raise NotImplementedError - @staticmethod @abc.abstractmethod def unpause(self) -> None: # pragma: NO COVER """ Unpauses this sequencer. @@ -51,12 +49,11 @@ def unpause(self) -> None: # pragma: NO COVER """ raise NotImplementedError - @staticmethod @abc.abstractmethod def publish( self, message: gapic_types.PubsubMessage, - retry: "retry.Retry" = None, + retry: "OptionalRetry" = gapic_v1.method.DEFAULT, timeout: gapic_types.TimeoutType = gapic_v1.method.DEFAULT, ) -> "futures.Future": # pragma: NO COVER """ Publish message for this ordering key. diff --git a/google/cloud/pubsub_v1/publisher/_sequencer/ordered_sequencer.py b/google/cloud/pubsub_v1/publisher/_sequencer/ordered_sequencer.py index 106c4da99..4d44b1a4f 100644 --- a/google/cloud/pubsub_v1/publisher/_sequencer/ordered_sequencer.py +++ b/google/cloud/pubsub_v1/publisher/_sequencer/ordered_sequencer.py @@ -14,21 +14,22 @@ import enum import collections -import concurrent.futures as futures import threading import typing -from typing import Iterable, Sequence +from typing import Deque, Iterable, Sequence from google.api_core import gapic_v1 +from google.cloud.pubsub_v1.publisher import futures from google.cloud.pubsub_v1.publisher import exceptions from google.cloud.pubsub_v1.publisher._sequencer import base as sequencer_base from google.cloud.pubsub_v1.publisher._batch import base as batch_base from google.pubsub_v1 import types as gapic_types if typing.TYPE_CHECKING: # pragma: NO COVER - from google.api_core import retry - from google.cloud.pubsub_v1 import PublisherClient + from google.cloud.pubsub_v1 import types from google.cloud.pubsub_v1.publisher import _batch + from google.cloud.pubsub_v1.publisher.client import Client as PublisherClient + from google.pubsub_v1.services.publisher.client import OptionalRetry class _OrderedSequencerStatus(str, enum.Enum): @@ -101,7 +102,7 @@ def __init__(self, client: "PublisherClient", topic: str, ordering_key: str): # Batches ordered from first (head/left) to last (right/tail). # Invariant: always has at least one batch after the first publish, # unless paused or stopped. - self._ordered_batches = collections.deque() + self._ordered_batches: Deque["_batch.thread.Batch"] = collections.deque() # See _OrderedSequencerStatus for valid state transitions. self._state = _OrderedSequencerStatus.ACCEPTING_MESSAGES @@ -237,8 +238,8 @@ def unpause(self) -> None: def _create_batch( self, - commit_retry: "retry.Retry" = gapic_v1.method.DEFAULT, - commit_timeout: gapic_types.TimeoutType = gapic_v1.method.DEFAULT, + commit_retry: "OptionalRetry" = gapic_v1.method.DEFAULT, + commit_timeout: "types.OptionalTimeout" = gapic_v1.method.DEFAULT, ) -> "_batch.thread.Batch": """ Create a new batch using the client's batch class and other stored settings. @@ -262,8 +263,8 @@ def _create_batch( def publish( self, message: gapic_types.PubsubMessage, - retry: "retry.Retry" = gapic_v1.method.DEFAULT, - timeout: gapic_types.TimeoutType = gapic_v1.method.DEFAULT, + retry: "OptionalRetry" = gapic_v1.method.DEFAULT, + timeout: "types.OptionalTimeout" = gapic_v1.method.DEFAULT, ) -> futures.Future: """ Publish message for this ordering key. @@ -289,12 +290,12 @@ def publish( """ with self._state_lock: if self._state == _OrderedSequencerStatus.PAUSED: - future = futures.Future() + errored_future = futures.Future() exception = exceptions.PublishToPausedOrderingKeyException( self._ordering_key ) - future.set_exception(exception) - return future + errored_future.set_exception(exception) + return errored_future # If waiting to be cleaned-up, convert to accepting messages to # prevent this sequencer from being cleaned-up only to have another diff --git a/google/cloud/pubsub_v1/publisher/_sequencer/unordered_sequencer.py b/google/cloud/pubsub_v1/publisher/_sequencer/unordered_sequencer.py index 91d47b948..7f2f13610 100644 --- a/google/cloud/pubsub_v1/publisher/_sequencer/unordered_sequencer.py +++ b/google/cloud/pubsub_v1/publisher/_sequencer/unordered_sequencer.py @@ -13,6 +13,7 @@ # limitations under the License. import typing +from typing import Optional from google.api_core import gapic_v1 @@ -20,10 +21,12 @@ from google.pubsub_v1 import types as gapic_types if typing.TYPE_CHECKING: # pragma: NO COVER - from concurrent import futures - from google.api_core import retry - from google.cloud.pubsub_v1 import PublisherClient from google.cloud.pubsub_v1.publisher import _batch + from google.cloud.pubsub_v1.publisher import futures + from google.cloud.pubsub_v1.publisher.client import Client as PublisherClient + from google.pubsub_v1.services.publisher.client import OptionalRetry + + from google.cloud.pubsub_v1 import types class UnorderedSequencer(base.Sequencer): @@ -35,7 +38,7 @@ class UnorderedSequencer(base.Sequencer): def __init__(self, client: "PublisherClient", topic: str): self._client = client self._topic = topic - self._current_batch = None + self._current_batch: Optional["_batch.thread.Batch"] = None self._stopped = False def is_finished(self) -> bool: @@ -88,8 +91,8 @@ def unpause(self) -> typing.NoReturn: def _create_batch( self, - commit_retry: "retry.Retry" = gapic_v1.method.DEFAULT, - commit_timeout: gapic_types.TimeoutType = gapic_v1.method.DEFAULT, + commit_retry: "OptionalRetry" = gapic_v1.method.DEFAULT, + commit_timeout: "types.OptionalTimeout" = gapic_v1.method.DEFAULT, ) -> "_batch.thread.Batch": """ Create a new batch using the client's batch class and other stored settings. @@ -113,8 +116,8 @@ def _create_batch( def publish( self, message: gapic_types.PubsubMessage, - retry: "retry.Retry" = gapic_v1.method.DEFAULT, - timeout: gapic_types.TimeoutType = gapic_v1.method.DEFAULT, + retry: "OptionalRetry" = gapic_v1.method.DEFAULT, + timeout: "types.OptionalTimeout" = gapic_v1.method.DEFAULT, ) -> "futures.Future": """ Batch message into existing or new batch. diff --git a/google/cloud/pubsub_v1/publisher/client.py b/google/cloud/pubsub_v1/publisher/client.py index 7e7c01c19..43305afcc 100644 --- a/google/cloud/pubsub_v1/publisher/client.py +++ b/google/cloud/pubsub_v1/publisher/client.py @@ -21,13 +21,13 @@ import threading import time import typing -from typing import Any, Sequence, Type, Union +from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union +import warnings from google.api_core import gapic_v1 -from google.auth.credentials import AnonymousCredentials -from google.oauth2 import service_account +from google.auth.credentials import AnonymousCredentials # type: ignore +from google.oauth2 import service_account # type: ignore -from google.cloud.pubsub_v1 import _gapic from google.cloud.pubsub_v1 import types from google.cloud.pubsub_v1.publisher import exceptions from google.cloud.pubsub_v1.publisher import futures @@ -46,25 +46,23 @@ __version__ = "0.0" if typing.TYPE_CHECKING: # pragma: NO COVER - from google import api_core from google.cloud import pubsub_v1 - from google.cloud.pubsub_v1.publisher._sequencer.base import Sequencer from google.cloud.pubsub_v1.publisher import _batch + from google.pubsub_v1.services.publisher.client import OptionalRetry + from google.pubsub_v1.types import pubsub as pubsub_types _LOGGER = logging.getLogger(__name__) -_DENYLISTED_METHODS = ( - "publish", - "from_service_account_file", - "from_service_account_json", -) _raw_proto_pubbsub_message = gapic_types.PubsubMessage.pb() +SequencerType = Union[ + ordered_sequencer.OrderedSequencer, unordered_sequencer.UnorderedSequencer +] -@_gapic.add_methods(publisher_client.PublisherClient, denylist=_DENYLISTED_METHODS) -class Client(object): + +class Client(publisher_client.PublisherClient): """A publisher client for Google Cloud Pub/Sub. This creates an object that is capable of publishing messages. @@ -143,8 +141,8 @@ def __init__( # Add the metrics headers, and instantiate the underlying GAPIC # client. - self.api = publisher_client.PublisherClient(**kwargs) - self._target = self.api._transport._host + super().__init__(**kwargs) + self._target = self._transport._host self._batch_class = thread.Batch self.batch_settings = types.BatchSettings(*batch_settings) @@ -152,16 +150,16 @@ def __init__( # messages. One batch exists for each topic. self._batch_lock = self._batch_class.make_lock() # (topic, ordering_key) => sequencers object - self._sequencers = {} + self._sequencers: Dict[Tuple[str, str], SequencerType] = {} self._is_stopped = False # Thread created to commit all sequencers after a timeout. - self._commit_thread = None + self._commit_thread: Optional[threading.Thread] = None # The object controlling the message publishing flow self._flow_controller = FlowController(self.publisher_options.flow_control) @classmethod - def from_service_account_file( + def from_service_account_file( # type: ignore[override] cls, filename: str, batch_settings: Union[types.BatchSettings, Sequence] = (), @@ -185,7 +183,7 @@ def from_service_account_file( kwargs["credentials"] = credentials return cls(batch_settings, **kwargs) - from_service_account_json = from_service_account_file + from_service_account_json = from_service_account_file # type: ignore[assignment] @property def target(self) -> str: @@ -196,7 +194,27 @@ def target(self) -> str: """ return self._target - def _get_or_create_sequencer(self, topic: str, ordering_key: str) -> "Sequencer": + @property + def api(self): + """The underlying gapic API client. + + .. versionchanged:: 2.10.0 + Instead of a GAPIC ``PublisherClient`` client instance, this property is a + proxy object to it with the same interface. + + .. deprecated:: 2.10.0 + Use the GAPIC methods and properties on the client instance directly + instead of through the :attr:`api` attribute. + """ + msg = ( + 'The "api" property only exists for backward compatibility, access its ' + 'attributes directly thorugh the client instance (e.g. "client.foo" ' + 'instead of "client.api.foo").' + ) + warnings.warn(msg, category=DeprecationWarning) + return super() + + def _get_or_create_sequencer(self, topic: str, ordering_key: str) -> SequencerType: """ Get an existing sequencer or create a new one given the (topic, ordering_key) pair. """ @@ -249,13 +267,17 @@ def resume_publish(self, topic: str, ordering_key: str) -> None: else: sequencer.unpause() - def publish( + def _gapic_publish(self, *args, **kwargs) -> "pubsub_types.PublishResponse": + """Call the GAPIC public API directly.""" + return super().publish(*args, **kwargs) + + def publish( # type: ignore[override] self, topic: str, data: bytes, ordering_key: str = "", - retry: "api_core.retry.Retry" = gapic_v1.method.DEFAULT, - timeout: gapic_types.TimeoutType = gapic_v1.method.DEFAULT, + retry: "OptionalRetry" = gapic_v1.method.DEFAULT, + timeout: "types.OptionalTimeout" = gapic_v1.method.DEFAULT, **attrs: Union[bytes, str], ) -> "pubsub_v1.publisher.futures.Future": """Publish a single message. @@ -379,9 +401,11 @@ def on_publish_done(future): if self._enable_message_ordering: if retry is gapic_v1.method.DEFAULT: # use the default retry for the publish GRPC method as a base - transport = self.api._transport - retry = transport._wrapped_methods[transport.publish]._retry - retry = retry.with_deadline(2.0 ** 32) + transport = self._transport + base_retry = transport._wrapped_methods[transport.publish]._retry + retry = base_retry.with_deadline(2.0 ** 32) + else: + retry = retry.with_deadline(2.0 ** 32) # Delegate the publishing to the sequencer. sequencer = self._get_or_create_sequencer(topic, ordering_key) @@ -490,7 +514,7 @@ def _set_batch_class(self, batch_class: Type) -> None: # Used only for testing. def _set_sequencer( - self, topic: str, sequencer: "Sequencer", ordering_key: str = "" + self, topic: str, sequencer: SequencerType, ordering_key: str = "" ) -> None: sequencer_key = (topic, ordering_key) self._sequencers[sequencer_key] = sequencer diff --git a/google/cloud/pubsub_v1/publisher/flow_controller.py b/google/cloud/pubsub_v1/publisher/flow_controller.py index fa3d58d33..baf6ba8ff 100644 --- a/google/cloud/pubsub_v1/publisher/flow_controller.py +++ b/google/cloud/pubsub_v1/publisher/flow_controller.py @@ -15,7 +15,7 @@ from collections import OrderedDict import logging import threading -from typing import Optional +from typing import Dict, Optional, Type import warnings from google.cloud.pubsub_v1 import types @@ -25,6 +25,9 @@ _LOGGER = logging.getLogger(__name__) +MessageType = Type[types.PubsubMessage] # type: ignore + + class _QuantityReservation: """A (partial) reservation of quantifiable resources.""" @@ -60,7 +63,7 @@ def __init__(self, settings: types.PublishFlowControl): # A FIFO queue of threads blocked on adding a message that also tracks their # reservations of available flow control bytes and message slots. # Only relevant if the configured limit exceeded behavior is BLOCK. - self._waiting = OrderedDict() + self._waiting: Dict[threading.Thread, _QuantityReservation] = OrderedDict() self._reserved_bytes = 0 self._reserved_slots = 0 @@ -72,7 +75,7 @@ def __init__(self, settings: types.PublishFlowControl): # The condition for blocking the flow if capacity is exceeded. self._has_capacity = threading.Condition(lock=self._operational_lock) - def add(self, message: types.PubsubMessage) -> None: # pytype: disable=module-attr + def add(self, message: MessageType) -> None: """Add a message to flow control. Adding a message updates the internal load statistics, and an action is @@ -166,9 +169,7 @@ def add(self, message: types.PubsubMessage) -> None: # pytype: disable=module-a self._reserved_slots -= 1 del self._waiting[current_thread] - def release( - self, message: types.PubsubMessage # pytype: disable=module-attr - ) -> None: + def release(self, message: MessageType) -> None: """Release a mesage from flow control. Args: @@ -255,9 +256,7 @@ def _ready_to_unblock(self) -> bool: return False - def _would_overflow( - self, message: types.PubsubMessage # pytype: disable=module-attr - ) -> bool: + def _would_overflow(self, message: MessageType) -> bool: """Determine if accepting a message would exceed flow control limits. The method assumes that the caller has obtained ``_operational_lock``. diff --git a/google/cloud/pubsub_v1/publisher/futures.py b/google/cloud/pubsub_v1/publisher/futures.py index 09bb2417c..c7cc66f18 100644 --- a/google/cloud/pubsub_v1/publisher/futures.py +++ b/google/cloud/pubsub_v1/publisher/futures.py @@ -14,10 +14,14 @@ from __future__ import absolute_import -from typing import Union +import typing +from typing import Any, Callable, Union from google.cloud.pubsub_v1 import futures +if typing.TYPE_CHECKING: # pragma: NO COVER + from google.cloud import pubsub_v1 + class Future(futures.Future): """This future object is returned from asychronous Pub/Sub publishing @@ -60,3 +64,20 @@ def result(self, timeout: Union[int, float] = None) -> str: call execution. """ return super().result(timeout=timeout) + + # This exists to make the type checkers happy. + def add_done_callback( + self, callback: Callable[["pubsub_v1.publisher.futures.Future"], Any] + ) -> None: + """Attach a callable that will be called when the future finishes. + + Args: + callback: + A callable that will be called with this future as its only + argument when the future completes or is cancelled. The callable + will always be called by a thread in the same process in which + it was added. If the future has already completed or been + cancelled then the callable will be called immediately. These + callables are called in the order that they were added. + """ + return super().add_done_callback(callback) # type: ignore diff --git a/google/cloud/pubsub_v1/subscriber/_protocol/dispatcher.py b/google/cloud/pubsub_v1/subscriber/_protocol/dispatcher.py index badcd78f3..6ab5165d1 100644 --- a/google/cloud/pubsub_v1/subscriber/_protocol/dispatcher.py +++ b/google/cloud/pubsub_v1/subscriber/_protocol/dispatcher.py @@ -15,17 +15,19 @@ from __future__ import absolute_import from __future__ import division -import collections +import functools import itertools import logging import math +import time import threading import typing -from typing import Sequence, Union +from typing import List, Optional, Sequence, Union +import warnings +from google.api_core.retry import exponential_sleep_generator from google.cloud.pubsub_v1.subscriber._protocol import helper_threads from google.cloud.pubsub_v1.subscriber._protocol import requests -from google.pubsub_v1 import types as gapic_types if typing.TYPE_CHECKING: # pragma: NO COVER import queue @@ -66,12 +68,20 @@ IDs at a time. """ +_MIN_EXACTLY_ONCE_DELIVERY_ACK_MODACK_RETRY_DURATION_SECS = 1 +"""The time to wait for the first retry of failed acks and modacks when exactly-once +delivery is enabled.""" + +_MAX_EXACTLY_ONCE_DELIVERY_ACK_MODACK_RETRY_DURATION_SECS = 10 * 60 +"""The maximum amount of time in seconds to retry failed acks and modacks when +exactly-once delivery is enabled.""" + class Dispatcher(object): def __init__(self, manager: "StreamingPullManager", queue: "queue.Queue"): self._manager = manager self._queue = queue - self._thread = None + self._thread: Optional[threading.Thread] = None self._operational_lock = threading.Lock() def start(self) -> None: @@ -112,25 +122,47 @@ def dispatch_callback(self, items: Sequence[RequestItem]) -> None: items: Queued requests to dispatch. """ - batched_commands = collections.defaultdict(list) + lease_requests: List[requests.LeaseRequest] = [] + modack_requests: List[requests.ModAckRequest] = [] + ack_requests: List[requests.AckRequest] = [] + nack_requests: List[requests.NackRequest] = [] + drop_requests: List[requests.DropRequest] = [] for item in items: - batched_commands[item.__class__].append(item) + if isinstance(item, requests.LeaseRequest): + lease_requests.append(item) + elif isinstance(item, requests.ModAckRequest): + modack_requests.append(item) + elif isinstance(item, requests.AckRequest): + ack_requests.append(item) + elif isinstance(item, requests.NackRequest): + nack_requests.append(item) + elif isinstance(item, requests.DropRequest): + drop_requests.append(item) + else: + warnings.warn( + f'Skipping unknown request item of type "{type(item)}"', + category=RuntimeWarning, + ) _LOGGER.debug("Handling %d batched requests", len(items)) - if batched_commands[requests.LeaseRequest]: - self.lease(batched_commands.pop(requests.LeaseRequest)) - if batched_commands[requests.ModAckRequest]: - self.modify_ack_deadline(batched_commands.pop(requests.ModAckRequest)) + if lease_requests: + self.lease(lease_requests) + + if modack_requests: + self.modify_ack_deadline(modack_requests) + # Note: Drop and ack *must* be after lease. It's possible to get both # the lease and the ack/drop request in the same batch. - if batched_commands[requests.AckRequest]: - self.ack(batched_commands.pop(requests.AckRequest)) - if batched_commands[requests.NackRequest]: - self.nack(batched_commands.pop(requests.NackRequest)) - if batched_commands[requests.DropRequest]: - self.drop(batched_commands.pop(requests.DropRequest)) + if ack_requests: + self.ack(ack_requests) + + if nack_requests: + self.nack(nack_requests) + + if drop_requests: + self.drop(drop_requests) def ack(self, items: Sequence[requests.AckRequest]) -> None: """Acknowledge the given messages. @@ -146,17 +178,66 @@ def ack(self, items: Sequence[requests.AckRequest]) -> None: # We must potentially split the request into multiple smaller requests # to avoid the server-side max request size limit. - ack_ids = (item.ack_id for item in items) + items_gen = iter(items) + ack_ids_gen = (item.ack_id for item in items) total_chunks = int(math.ceil(len(items) / _ACK_IDS_BATCH_SIZE)) for _ in range(total_chunks): - request = gapic_types.StreamingPullRequest( - ack_ids=itertools.islice(ack_ids, _ACK_IDS_BATCH_SIZE) + ack_reqs_dict = { + req.ack_id: req + for req in itertools.islice(items_gen, _ACK_IDS_BATCH_SIZE) + } + requests_completed, requests_to_retry = self._manager.send_unary_ack( + ack_ids=list(itertools.islice(ack_ids_gen, _ACK_IDS_BATCH_SIZE)), + ack_reqs_dict=ack_reqs_dict, + ) + + # Remove the completed messages from lease management. + self.drop(requests_completed) + + # Retry on a separate thread so the dispatcher thread isn't blocked + # by sleeps. + if requests_to_retry: + self._start_retry_thread( + "Thread-RetryAcks", + functools.partial(self._retry_acks, requests_to_retry), + ) + + def _start_retry_thread(self, thread_name, thread_target): + # note: if the thread is *not* a daemon, a memory leak exists due to a cpython issue. + # https://github.com/googleapis/python-pubsub/issues/395#issuecomment-829910303 + # https://github.com/googleapis/python-pubsub/issues/395#issuecomment-830092418 + retry_thread = threading.Thread( + name=thread_name, target=thread_target, daemon=True, + ) + # The thread finishes when the requests succeed or eventually fail with + # a back-end timeout error or other permanent failure. + retry_thread.start() + + def _retry_acks(self, requests_to_retry): + retry_delay_gen = exponential_sleep_generator( + initial=_MIN_EXACTLY_ONCE_DELIVERY_ACK_MODACK_RETRY_DURATION_SECS, + maximum=_MAX_EXACTLY_ONCE_DELIVERY_ACK_MODACK_RETRY_DURATION_SECS, + ) + while requests_to_retry: + time_to_wait = next(retry_delay_gen) + _LOGGER.debug( + "Retrying {len(requests_to_retry)} ack(s) after delay of " + + str(time_to_wait) + + " seconds" ) - self._manager.send(request) + time.sleep(time_to_wait) - # Remove the message from lease management. - self.drop(items) + ack_reqs_dict = {req.ack_id: req for req in requests_to_retry} + requests_completed, requests_to_retry = self._manager.send_unary_ack( + ack_ids=[req.ack_id for req in requests_to_retry], + ack_reqs_dict=ack_reqs_dict, + ) + assert ( + len(requests_to_retry) <= _ACK_IDS_BATCH_SIZE + ), "Too many requests to be retried." + # Remove the completed messages from lease management. + self.drop(requests_completed) def drop( self, @@ -169,6 +250,7 @@ def drop( Args: items: The items to drop. """ + assert self._manager.leaser is not None self._manager.leaser.remove(items) ordering_keys = (k.ordering_key for k in items if k.ordering_key) self._manager.activate_ordering_keys(ordering_keys) @@ -180,6 +262,7 @@ def lease(self, items: Sequence[requests.LeaseRequest]) -> None: Args: items: The items to lease. """ + assert self._manager.leaser is not None self._manager.leaser.add(items) self._manager.maybe_pause_consumer() @@ -191,16 +274,58 @@ def modify_ack_deadline(self, items: Sequence[requests.ModAckRequest]) -> None: """ # We must potentially split the request into multiple smaller requests # to avoid the server-side max request size limit. - ack_ids = (item.ack_id for item in items) - seconds = (item.seconds for item in items) + items_gen = iter(items) + ack_ids_gen = (item.ack_id for item in items) + deadline_seconds_gen = (item.seconds for item in items) total_chunks = int(math.ceil(len(items) / _ACK_IDS_BATCH_SIZE)) for _ in range(total_chunks): - request = gapic_types.StreamingPullRequest( - modify_deadline_ack_ids=itertools.islice(ack_ids, _ACK_IDS_BATCH_SIZE), - modify_deadline_seconds=itertools.islice(seconds, _ACK_IDS_BATCH_SIZE), + ack_reqs_dict = { + req.ack_id: req + for req in itertools.islice(items_gen, _ACK_IDS_BATCH_SIZE) + } + # no further work needs to be done for `requests_to_retry` + requests_completed, requests_to_retry = self._manager.send_unary_modack( + modify_deadline_ack_ids=list( + itertools.islice(ack_ids_gen, _ACK_IDS_BATCH_SIZE) + ), + modify_deadline_seconds=list( + itertools.islice(deadline_seconds_gen, _ACK_IDS_BATCH_SIZE) + ), + ack_reqs_dict=ack_reqs_dict, + ) + assert ( + len(requests_to_retry) <= _ACK_IDS_BATCH_SIZE + ), "Too many requests to be retried." + + # Retry on a separate thread so the dispatcher thread isn't blocked + # by sleeps. + if requests_to_retry: + self._start_retry_thread( + "Thread-RetryModAcks", + functools.partial(self._retry_modacks, requests_to_retry), + ) + + def _retry_modacks(self, requests_to_retry): + retry_delay_gen = exponential_sleep_generator( + initial=_MIN_EXACTLY_ONCE_DELIVERY_ACK_MODACK_RETRY_DURATION_SECS, + maximum=_MAX_EXACTLY_ONCE_DELIVERY_ACK_MODACK_RETRY_DURATION_SECS, + ) + while requests_to_retry: + time_to_wait = next(retry_delay_gen) + _LOGGER.debug( + "Retrying {len(requests_to_retry)} modack(s) after delay of " + + str(time_to_wait) + + " seconds" + ) + time.sleep(time_to_wait) + + ack_reqs_dict = {req.ack_id: req for req in requests_to_retry} + requests_completed, requests_to_retry = self._manager.send_unary_modack( + modify_deadline_ack_ids=[req.ack_id for req in requests_to_retry], + modify_deadline_seconds=[req.seconds for req in requests_to_retry], + ack_reqs_dict=ack_reqs_dict, ) - self._manager.send(request) def nack(self, items: Sequence[requests.NackRequest]) -> None: """Explicitly deny receipt of messages. @@ -209,6 +334,20 @@ def nack(self, items: Sequence[requests.NackRequest]) -> None: items: The items to deny. """ self.modify_ack_deadline( - [requests.ModAckRequest(ack_id=item.ack_id, seconds=0) for item in items] + [ + requests.ModAckRequest( + ack_id=item.ack_id, seconds=0, future=item.future + ) + for item in items + ] + ) + self.drop( + [ + requests.DropRequest( + ack_id=item.ack_id, + byte_size=item.byte_size, + ordering_key=item.ordering_key, + ) + for item in items + ] ) - self.drop([requests.DropRequest(*item) for item in items]) diff --git a/google/cloud/pubsub_v1/subscriber/_protocol/heartbeater.py b/google/cloud/pubsub_v1/subscriber/_protocol/heartbeater.py index 842e4adc5..0ab03ddf9 100644 --- a/google/cloud/pubsub_v1/subscriber/_protocol/heartbeater.py +++ b/google/cloud/pubsub_v1/subscriber/_protocol/heartbeater.py @@ -17,6 +17,7 @@ import logging import threading import typing +from typing import Optional if typing.TYPE_CHECKING: # pragma: NO COVER from google.cloud.pubsub_v1.subscriber._protocol.streaming_pull_manager import ( @@ -34,7 +35,7 @@ class Heartbeater(object): def __init__(self, manager: "StreamingPullManager", period: int = _DEFAULT_PERIOD): - self._thread = None + self._thread: Optional[threading.Thread] = None self._operational_lock = threading.Lock() self._manager = manager self._stop_event = threading.Event() diff --git a/google/cloud/pubsub_v1/subscriber/_protocol/leaser.py b/google/cloud/pubsub_v1/subscriber/_protocol/leaser.py index c8d7e9365..de110e992 100644 --- a/google/cloud/pubsub_v1/subscriber/_protocol/leaser.py +++ b/google/cloud/pubsub_v1/subscriber/_protocol/leaser.py @@ -14,14 +14,21 @@ from __future__ import absolute_import -import collections import copy import logging import random import threading import time import typing -from typing import Iterable, Sequence, Union +from typing import Dict, Iterable, Optional, Union + +try: + from collections.abc import KeysView + + KeysView[None] # KeysView is only subscriptable in Python 3.9+ +except TypeError: + # Deprecated since Python 3.9, thus only use as a fallback in older Python versions + from typing import KeysView from google.cloud.pubsub_v1.subscriber._protocol import requests @@ -35,14 +42,17 @@ _LEASE_WORKER_NAME = "Thread-LeaseMaintainer" -_LeasedMessage = collections.namedtuple( - "_LeasedMessage", ["sent_time", "size", "ordering_key"] -) +class _LeasedMessage(typing.NamedTuple): + sent_time: float + """The local time when ACK ID was initially leased in seconds since the epoch.""" + + size: int + ordering_key: Optional[str] class Leaser(object): def __init__(self, manager: "StreamingPullManager"): - self._thread = None + self._thread: Optional[threading.Thread] = None self._manager = manager # a lock used for start/stop operations, protecting the _thread attribute @@ -53,11 +63,10 @@ def __init__(self, manager: "StreamingPullManager"): self._add_remove_lock = threading.Lock() # Dict of ack_id -> _LeasedMessage - self._leased_messages = {} - """dict[str, float]: A mapping of ack IDs to the local time when the - ack ID was initially leased in seconds since the epoch.""" + self._leased_messages: Dict[str, _LeasedMessage] = {} + self._bytes = 0 - """int: The total number of bytes consumed by leased messages.""" + """The total number of bytes consumed by leased messages.""" self._stop_event = threading.Event() @@ -67,7 +76,7 @@ def message_count(self) -> int: return len(self._leased_messages) @property - def ack_ids(self) -> Sequence[str]: + def ack_ids(self) -> KeysView[str]: """The ack IDs of all leased messages.""" return self._leased_messages.keys() @@ -163,6 +172,7 @@ def maintain_leases(self) -> None: _LOGGER.warning( "Dropping %s items because they were leased too long.", len(to_drop) ) + assert self._manager.dispatcher is not None self._manager.dispatcher.drop(to_drop) # Remove dropped items from our copy of the leased messages (they @@ -171,7 +181,7 @@ def maintain_leases(self) -> None: for item in to_drop: leased_messages.pop(item.ack_id) - # Create a streaming pull request. + # Create a modack request. # We do not actually call `modify_ack_deadline` over and over # because it is more efficient to make a single request. ack_ids = leased_messages.keys() @@ -183,9 +193,9 @@ def maintain_leases(self) -> None: # without any sort of race condition would require a # way for ``send_request`` to fail when the consumer # is inactive. - self._manager.dispatcher.modify_ack_deadline( - [requests.ModAckRequest(ack_id, deadline) for ack_id in ack_ids] - ) + assert self._manager.dispatcher is not None + ack_id_gen = (ack_id for ack_id in ack_ids) + self._manager._send_lease_modacks(ack_id_gen, deadline) # Now wait an appropriate period of time and do this again. # diff --git a/google/cloud/pubsub_v1/subscriber/_protocol/requests.py b/google/cloud/pubsub_v1/subscriber/_protocol/requests.py index 7481d95a9..9cd387545 100644 --- a/google/cloud/pubsub_v1/subscriber/_protocol/requests.py +++ b/google/cloud/pubsub_v1/subscriber/_protocol/requests.py @@ -12,8 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import typing from typing import NamedTuple, Optional +if typing.TYPE_CHECKING: # pragma: NO COVER + from google.cloud.pubsub_v1.subscriber import futures + # Namedtuples for management requests. Used by the Message class to communicate # items of work back to the policy. @@ -22,6 +26,7 @@ class AckRequest(NamedTuple): byte_size: int time_to_ack: float ordering_key: Optional[str] + future: Optional["futures.Future"] class DropRequest(NamedTuple): @@ -39,9 +44,11 @@ class LeaseRequest(NamedTuple): class ModAckRequest(NamedTuple): ack_id: str seconds: float + future: Optional["futures.Future"] class NackRequest(NamedTuple): ack_id: str byte_size: int ordering_key: Optional[str] + future: Optional["futures.Future"] diff --git a/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py b/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py index 018917b90..5a9d08026 100644 --- a/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py +++ b/google/cloud/pubsub_v1/subscriber/_protocol/streaming_pull_manager.py @@ -20,10 +20,10 @@ import logging import threading import typing -from typing import Any, Callable, Iterable +from typing import Any, Callable, Iterable, List, Optional, Tuple, Union import uuid -import grpc +import grpc # type: ignore from google.api_core import bidi from google.api_core import exceptions @@ -34,13 +34,22 @@ from google.cloud.pubsub_v1.subscriber._protocol import leaser from google.cloud.pubsub_v1.subscriber._protocol import messages_on_hold from google.cloud.pubsub_v1.subscriber._protocol import requests +from google.cloud.pubsub_v1.subscriber.exceptions import ( + AcknowledgeError, + AcknowledgeStatus, +) import google.cloud.pubsub_v1.subscriber.message -import google.cloud.pubsub_v1.subscriber.scheduler +from google.cloud.pubsub_v1.subscriber import futures +from google.cloud.pubsub_v1.subscriber.scheduler import ThreadScheduler from google.pubsub_v1 import types as gapic_types +from grpc_status import rpc_status # type: ignore +from google.rpc.error_details_pb2 import ErrorInfo # type: ignore +from google.rpc import code_pb2 # type: ignore +from google.rpc import status_pb2 if typing.TYPE_CHECKING: # pragma: NO COVER from google.cloud.pubsub_v1 import subscriber - from google.cloud.pubsub_v1.subscriber.scheduler import Scheduler + from google.protobuf.internal import containers _LOGGER = logging.getLogger(__name__) @@ -61,6 +70,11 @@ _RESUME_THRESHOLD = 0.8 """The load threshold below which to resume the incoming message stream.""" +_MIN_ACK_DEADLINE_SECS_WHEN_EXACTLY_ONCE_ENABLED = 60 +"""The minimum ack_deadline, in seconds, for when exactly_once is enabled for +a subscription. We do this to reduce premature ack expiration. +""" + def _wrap_as_exception(maybe_exception: Any) -> BaseException: """Wrap an object as a Python exception, if needed. @@ -105,6 +119,86 @@ def _wrap_callback_errors( on_callback_error(exc) +def _get_status(exc: exceptions.GoogleAPICallError,) -> Optional["status_pb2.Status"]: + if not exc.response: + _LOGGER.debug("No response obj in errored RPC call.") + return None + try: + return rpc_status.from_call(exc.response) + # Possible "If the gRPC call’s code or details are inconsistent + # with the status code and message inside of the + # google.rpc.status.Status" + except ValueError: + _LOGGER.debug("ValueError when parsing ErrorInfo.", exc_info=True) + return None + + +def _get_ack_errors( + exc: exceptions.GoogleAPICallError, +) -> Optional["containers.ScalarMap"]: + status = _get_status(exc) + if not status: + _LOGGER.debug("Unable to get status of errored RPC.") + return None + for detail in status.details: + info = ErrorInfo() + if not (detail.Is(ErrorInfo.DESCRIPTOR) and detail.Unpack(info)): + _LOGGER.debug("Unable to unpack ErrorInfo.") + return None + return info.metadata + return None + + +def _process_requests( + error_status: Optional["status_pb2.Status"], + ack_reqs_dict: "containers.ScalarMap", + errors_dict: Optional["containers.ScalarMap"], +): + """Process requests by referring to error_status and errors_dict. + + The errors returned by the server in as `error_status` or in `errors_dict` + are used to complete the request futures in `ack_reqs_dict` (with a success + or exception) or to return requests for further retries. + """ + requests_completed = [] + requests_to_retry = [] + for ack_id in ack_reqs_dict: + if errors_dict and ack_id in errors_dict: + exactly_once_error = errors_dict[ack_id] + if exactly_once_error.startswith("TRANSIENT_"): + requests_to_retry.append(ack_reqs_dict[ack_id]) + else: + if exactly_once_error == "PERMANENT_FAILURE_INVALID_ACK_ID": + exc = AcknowledgeError(AcknowledgeStatus.INVALID_ACK_ID, info=None) + else: + exc = AcknowledgeError(AcknowledgeStatus.OTHER, exactly_once_error) + + future = ack_reqs_dict[ack_id].future + future.set_exception(exc) + requests_completed.append(ack_reqs_dict[ack_id]) + elif error_status: + # Only permanent errors are expected here b/c retriable errors are + # retried at the lower, GRPC level. + if error_status.code == code_pb2.PERMISSION_DENIED: + exc = AcknowledgeError(AcknowledgeStatus.PERMISSION_DENIED, info=None) + elif error_status.code == code_pb2.FAILED_PRECONDITION: + exc = AcknowledgeError(AcknowledgeStatus.FAILED_PRECONDITION, info=None) + else: + exc = AcknowledgeError(AcknowledgeStatus.OTHER, str(error_status)) + future = ack_reqs_dict[ack_id].future + future.set_exception(exc) + requests_completed.append(ack_reqs_dict[ack_id]) + elif ack_reqs_dict[ack_id].future: + future = ack_reqs_dict[ack_id].future + # success + future.set_result(AcknowledgeStatus.SUCCESS) + requests_completed.append(ack_reqs_dict[ack_id]) + else: + requests_completed.append(ack_reqs_dict[ack_id]) + + return requests_completed, requests_to_retry + + class StreamingPullManager(object): """The streaming pull manager coordinates pulling messages from Pub/Sub, leasing them, and scheduling them to be processed. @@ -143,24 +237,29 @@ def __init__( client: "subscriber.Client", subscription: str, flow_control: types.FlowControl = types.FlowControl(), - scheduler: "Scheduler" = None, + scheduler: ThreadScheduler = None, use_legacy_flow_control: bool = False, await_callbacks_on_shutdown: bool = False, ): self._client = client self._subscription = subscription + self._exactly_once_enabled = False self._flow_control = flow_control self._use_legacy_flow_control = use_legacy_flow_control self._await_callbacks_on_shutdown = await_callbacks_on_shutdown self._ack_histogram = histogram.Histogram() self._last_histogram_size = 0 - self._ack_deadline = histogram.MIN_ACK_DEADLINE - self._rpc = None - self._callback = None + self._ack_deadline: Union[int, float] = histogram.MIN_ACK_DEADLINE + self._rpc: Optional[bidi.ResumableBidiRpc] = None + self._callback: Optional[functools.partial] = None self._closing = threading.Lock() self._closed = False - self._close_callbacks = [] - self._regular_shutdown_thread = None # Created on intentional shutdown. + self._close_callbacks: List[Callable[["StreamingPullManager", Any], Any]] = [] + # Guarded by self._exactly_once_enabled_lock + self._send_new_ack_deadline = False + + # A shutdown thread is created on intentional shutdown. + self._regular_shutdown_thread: Optional[threading.Thread] = None # Generate a random client id tied to this object. All streaming pull # connections (initial and re-connects) will then use the same client @@ -169,9 +268,7 @@ def __init__( self._client_id = str(uuid.uuid4()) if scheduler is None: - self._scheduler = ( - google.cloud.pubsub_v1.subscriber.scheduler.ThreadScheduler() - ) + self._scheduler: Optional[ThreadScheduler] = ThreadScheduler() else: self._scheduler = scheduler @@ -190,16 +287,22 @@ def __init__( # currently on hold. self._pause_resume_lock = threading.Lock() + # A lock guarding the self._exactly_once_enabled variable. We may also + # acquire the self._ack_deadline_lock while this lock is held, but not + # the reverse. So, we maintain a simple ordering of these two locks to + # prevent deadlocks. + self._exactly_once_enabled_lock = threading.Lock() + # A lock protecting the current ACK deadline used in the lease management. This # value can be potentially updated both by the leaser thread and by the message # consumer thread when invoking the internal _on_response() callback. self._ack_deadline_lock = threading.Lock() # The threads created in ``.open()``. - self._dispatcher = None - self._leaser = None - self._consumer = None - self._heartbeater = None + self._dispatcher: Optional[dispatcher.Dispatcher] = None + self._leaser: Optional[leaser.Leaser] = None + self._consumer: Optional[bidi.BackgroundConsumer] = None + self._heartbeater: Optional[heartbeater.Heartbeater] = None @property def is_active(self) -> bool: @@ -216,12 +319,12 @@ def flow_control(self) -> types.FlowControl: return self._flow_control @property - def dispatcher(self) -> dispatcher.Dispatcher: + def dispatcher(self) -> Optional[dispatcher.Dispatcher]: """The dispatcher helper.""" return self._dispatcher @property - def leaser(self) -> leaser.Leaser: + def leaser(self) -> Optional[leaser.Leaser]: """The leaser helper.""" return self._leaser @@ -274,6 +377,22 @@ def _obtain_ack_deadline(self, maybe_update: bool) -> float: histogram.MIN_ACK_DEADLINE, ) self._ack_deadline = min(self._ack_deadline, flow_control_setting) + + # If the user explicitly sets a min ack_deadline, respect it. + if self.flow_control.min_duration_per_lease_extension > 0: + # The setting in flow control could be too high, adjust if needed. + flow_control_setting = min( + self.flow_control.min_duration_per_lease_extension, + histogram.MAX_ACK_DEADLINE, + ) + self._ack_deadline = max(self._ack_deadline, flow_control_setting) + elif self._exactly_once_enabled: + # Higher minimum ack_deadline for subscriptions with + # exactly-once delivery enabled. + self._ack_deadline = max( + self._ack_deadline, _MIN_ACK_DEADLINE_SECS_WHEN_EXACTLY_ONCE_ENABLED + ) + return self._ack_deadline @property @@ -312,7 +431,7 @@ def load(self) -> float: ) def add_close_callback( - self, callback: Callable[["StreamingPullManager", Any], Any], + self, callback: Callable[["StreamingPullManager", Any], Any] ) -> None: """Schedules a callable when the manager closes. @@ -401,6 +520,8 @@ def _maybe_release_messages(self) -> None: self._schedule_message_on_hold(msg) released_ack_ids.append(msg.ack_id) + + assert self._leaser is not None self._leaser.start_lease_expiry_timer(released_ack_ids) def _schedule_message_on_hold( @@ -430,25 +551,71 @@ def _schedule_message_on_hold( self._messages_on_hold.size, self._on_hold_bytes, ) + assert self._scheduler is not None + assert self._callback is not None self._scheduler.schedule(self._callback, msg) - def _send_unary_request(self, request: gapic_types.StreamingPullRequest) -> None: + def send_unary_ack( + self, ack_ids, ack_reqs_dict + ) -> Tuple[List[requests.AckRequest], List[requests.AckRequest]]: """Send a request using a separate unary request instead of over the stream. - Args: - request: The stream request to be mapped into unary requests. + If a RetryError occurs, the manager shutdown is triggered, and the + error is re-raised. """ - if request.ack_ids: - self._client.acknowledge( - subscription=self._subscription, ack_ids=list(request.ack_ids) + assert ack_ids + assert len(ack_ids) == len(ack_reqs_dict) + + error_status = None + ack_errors_dict = None + try: + self._client.acknowledge(subscription=self._subscription, ack_ids=ack_ids) + except exceptions.GoogleAPICallError as exc: + _LOGGER.debug( + "Exception while sending unary RPC. This is typically " + "non-fatal as stream requests are best-effort.", + exc_info=True, + ) + error_status = _get_status(exc) + ack_errors_dict = _get_ack_errors(exc) + except exceptions.RetryError as exc: + status = status_pb2.Status() + status.code = code_pb2.DEADLINE_EXCEEDED + # Makes sure to complete futures so they don't block forever. + _process_requests(status, ack_reqs_dict, None) + _LOGGER.debug( + "RetryError while sending unary RPC. Waiting on a transient " + "error resolution for too long, will now trigger shutdown.", + exc_info=False, ) + # The underlying channel has been suffering from a retryable error + # for too long, time to give up and shut the streaming pull down. + self._on_rpc_done(exc) + raise - if request.modify_deadline_ack_ids: + requests_completed, requests_to_retry = _process_requests( + error_status, ack_reqs_dict, ack_errors_dict + ) + return requests_completed, requests_to_retry + + def send_unary_modack( + self, modify_deadline_ack_ids, modify_deadline_seconds, ack_reqs_dict + ) -> Tuple[List[requests.ModAckRequest], List[requests.ModAckRequest]]: + """Send a request using a separate unary request instead of over the stream. + + If a RetryError occurs, the manager shutdown is triggered, and the + error is re-raised. + """ + assert modify_deadline_ack_ids + + error_status = None + modack_errors_dict = None + try: # Send ack_ids with the same deadline seconds together. deadline_to_ack_ids = collections.defaultdict(list) - for n, ack_id in enumerate(request.modify_deadline_ack_ids): - deadline = request.modify_deadline_seconds[n] + for n, ack_id in enumerate(modify_deadline_ack_ids): + deadline = modify_deadline_seconds[n] deadline_to_ack_ids[deadline].append(ack_id) for deadline, ack_ids in deadline_to_ack_ids.items(): @@ -457,24 +624,19 @@ def _send_unary_request(self, request: gapic_types.StreamingPullRequest) -> None ack_ids=ack_ids, ack_deadline_seconds=deadline, ) - - _LOGGER.debug("Sent request(s) over unary RPC.") - - def send(self, request: gapic_types.StreamingPullRequest) -> None: - """Queue a request to be sent to the RPC. - - If a RetryError occurs, the manager shutdown is triggered, and the - error is re-raised. - """ - try: - self._send_unary_request(request) - except exceptions.GoogleAPICallError: + except exceptions.GoogleAPICallError as exc: _LOGGER.debug( "Exception while sending unary RPC. This is typically " "non-fatal as stream requests are best-effort.", exc_info=True, ) + error_status = _get_status(exc) + modack_errors_dict = _get_ack_errors(exc) except exceptions.RetryError as exc: + status = status_pb2.Status() + status.code = code_pb2.DEADLINE_EXCEEDED + # Makes sure to complete futures so they don't block forever. + _process_requests(status, ack_reqs_dict, None) _LOGGER.debug( "RetryError while sending unary RPC. Waiting on a transient " "error resolution for too long, will now trigger shutdown.", @@ -485,14 +647,34 @@ def send(self, request: gapic_types.StreamingPullRequest) -> None: self._on_rpc_done(exc) raise + requests_completed, requests_to_retry = _process_requests( + error_status, ack_reqs_dict, modack_errors_dict + ) + return requests_completed, requests_to_retry + def heartbeat(self) -> bool: - """Sends an empty request over the streaming pull RPC. + """Sends a heartbeat request over the streaming pull RPC. + + The request is empty by default, but may contain the current ack_deadline + if the self._exactly_once_enabled flag has changed. Returns: If a heartbeat request has actually been sent. """ if self._rpc is not None and self._rpc.is_active: - self._rpc.send(gapic_types.StreamingPullRequest()) + send_new_ack_deadline = False + with self._exactly_once_enabled_lock: + send_new_ack_deadline = self._send_new_ack_deadline + self._send_new_ack_deadline = False + + if send_new_ack_deadline: + request = gapic_types.StreamingPullRequest( + stream_ack_deadline_seconds=self.ack_deadline + ) + else: + request = gapic_types.StreamingPullRequest() + + self._rpc.send(request) return True return False @@ -529,7 +711,7 @@ def open( self._get_initial_request, stream_ack_deadline_seconds ) self._rpc = bidi.ResumableBidiRpc( - start_rpc=self._client.api.streaming_pull, + start_rpc=self._client.streaming_pull, initial_request=get_initial_request, should_recover=self._should_recover, should_terminate=self._should_terminate, @@ -544,13 +726,12 @@ def open( ) # Create references to threads - # pytype: disable=wrong-arg-types - # (pytype incorrectly complains about "self" not being the right argument type) - self._dispatcher = dispatcher.Dispatcher(self, self._scheduler.queue) + assert self._scheduler is not None + scheduler_queue = self._scheduler.queue + self._dispatcher = dispatcher.Dispatcher(self, scheduler_queue) self._consumer = bidi.BackgroundConsumer(self._rpc, self._on_response) self._leaser = leaser.Leaser(self) self._heartbeater = heartbeater.Heartbeater(self) - # pytype: enable=wrong-arg-types # Start the thread to pass the requests. self._dispatcher.start() @@ -601,11 +782,13 @@ def _shutdown(self, reason: Any = None) -> None: # Stop consuming messages. if self.is_active: _LOGGER.debug("Stopping consumer.") + assert self._consumer is not None self._consumer.stop() self._consumer = None # Shutdown all helper threads _LOGGER.debug("Stopping scheduler.") + assert self._scheduler is not None dropped_messages = self._scheduler.shutdown( await_msg_callbacks=self._await_callbacks_on_shutdown ) @@ -621,6 +804,7 @@ def _shutdown(self, reason: Any = None) -> None: # for the manager's maybe_resume_consumer() / maybe_pause_consumer(), # because the consumer gets shut down first. _LOGGER.debug("Stopping leaser.") + assert self._leaser is not None self._leaser.stop() total = len(dropped_messages) + len( @@ -634,12 +818,14 @@ def _shutdown(self, reason: Any = None) -> None: msg.nack() _LOGGER.debug("Stopping dispatcher.") + assert self._dispatcher is not None self._dispatcher.stop() self._dispatcher = None # dispatcher terminated, OK to dispose the leaser reference now self._leaser = None _LOGGER.debug("Stopping heartbeater.") + assert self._heartbeater is not None self._heartbeater.stop() self._heartbeater = None @@ -693,6 +879,42 @@ def _get_initial_request( # Return the initial request. return request + def _send_lease_modacks(self, ack_ids: Iterable[str], ack_deadline: float): + exactly_once_enabled = False + with self._exactly_once_enabled_lock: + exactly_once_enabled = self._exactly_once_enabled + if exactly_once_enabled: + items = [] + for ack_id in ack_ids: + future = futures.Future() + request = requests.ModAckRequest(ack_id, ack_deadline, future) + items.append(request) + + assert self._dispatcher is not None + self._dispatcher.modify_ack_deadline(items) + + for req in items: + try: + assert req.future is not None + req.future.result() + except AcknowledgeError: + _LOGGER.warning( + "AcknowledgeError when lease-modacking a message.", + exc_info=True, + ) + else: + items = [ + requests.ModAckRequest(ack_id, self.ack_deadline, None) + for ack_id in ack_ids + ] + assert self._dispatcher is not None + self._dispatcher.modify_ack_deadline(items) + + def _exactly_once_delivery_enabled(self) -> bool: + """Whether exactly-once delivery is enabled for the subscription.""" + with self._exactly_once_enabled_lock: + return self._exactly_once_enabled + def _on_response(self, response: gapic_types.StreamingPullResponse) -> None: """Process all received Pub/Sub messages. @@ -723,22 +945,36 @@ def _on_response(self, response: gapic_types.StreamingPullResponse) -> None: self._on_hold_bytes, ) + with self._exactly_once_enabled_lock: + if ( + response.subscription_properties.exactly_once_delivery_enabled + != self._exactly_once_enabled + ): + self._exactly_once_enabled = ( + response.subscription_properties.exactly_once_delivery_enabled + ) + # Update ack_deadline, whose minimum depends on self._exactly_once_enabled + # This method acquires the self._ack_deadline_lock lock. + self._obtain_ack_deadline(maybe_update=True) + self._send_new_ack_deadline = True + # Immediately (i.e. without waiting for the auto lease management) # modack the messages we received, as this tells the server that we've # received them. - items = [ - requests.ModAckRequest(message.ack_id, self.ack_deadline) - for message in received_messages - ] - self._dispatcher.modify_ack_deadline(items) + ack_id_gen = (message.ack_id for message in received_messages) + self._send_lease_modacks(ack_id_gen, self.ack_deadline) with self._pause_resume_lock: + assert self._scheduler is not None + assert self._leaser is not None + for received_message in received_messages: message = google.cloud.pubsub_v1.subscriber.message.Message( received_message.message, received_message.ack_id, received_message.delivery_attempt, self._scheduler.queue, + self._exactly_once_delivery_enabled, ) self._messages_on_hold.put(message) self._on_hold_bytes += message.size @@ -747,13 +983,13 @@ def _on_response(self, response: gapic_types.StreamingPullResponse) -> None: byte_size=message.size, ordering_key=message.ordering_key, ) - self.leaser.add([req]) + self._leaser.add([req]) self._maybe_release_messages() self.maybe_pause_consumer() - def _should_recover(self, exception: Exception) -> bool: + def _should_recover(self, exception: BaseException) -> bool: """Determine if an error on the RPC stream should be recovered. If the exception is one of the retryable exceptions, this will signal @@ -775,7 +1011,7 @@ def _should_recover(self, exception: Exception) -> bool: _LOGGER.info("Observed non-recoverable stream error %s", exception) return False - def _should_terminate(self, exception: Exception) -> bool: + def _should_terminate(self, exception: BaseException) -> bool: """Determine if an error on the RPC stream should be terminated. If the exception is one of the terminating exceptions, this will signal diff --git a/google/cloud/pubsub_v1/subscriber/client.py b/google/cloud/pubsub_v1/subscriber/client.py index 099a65318..9c12a0bfb 100644 --- a/google/cloud/pubsub_v1/subscriber/client.py +++ b/google/cloud/pubsub_v1/subscriber/client.py @@ -17,12 +17,12 @@ import os import pkg_resources import typing -from typing import Any, Callable, Optional, Sequence, Union +from typing import cast, Any, Callable, Optional, Sequence, Union +import warnings -from google.auth.credentials import AnonymousCredentials -from google.oauth2 import service_account +from google.auth.credentials import AnonymousCredentials # type: ignore +from google.oauth2 import service_account # type: ignore -from google.cloud.pubsub_v1 import _gapic from google.cloud.pubsub_v1 import types from google.cloud.pubsub_v1.subscriber import futures from google.cloud.pubsub_v1.subscriber._protocol import streaming_pull_manager @@ -30,6 +30,9 @@ if typing.TYPE_CHECKING: # pragma: NO COVER from google.cloud.pubsub_v1 import subscriber + from google.pubsub_v1.services.subscriber.transports.grpc import ( + SubscriberGrpcTransport, + ) try: @@ -39,15 +42,8 @@ # a PIP package. __version__ = "0.0" -_DENYLISTED_METHODS = ( - "publish", - "from_service_account_file", - "from_service_account_json", -) - -@_gapic.add_methods(subscriber_client.SubscriberClient, denylist=_DENYLISTED_METHODS) -class Client(object): +class Client(subscriber_client.SubscriberClient): """A subscriber client for Google Cloud Pub/Sub. This creates an object that is capable of subscribing to messages. @@ -88,12 +84,14 @@ def __init__(self, **kwargs: Any): kwargs["credentials"] = AnonymousCredentials() # Instantiate the underlying GAPIC client. - self._api = subscriber_client.SubscriberClient(**kwargs) - self._target = self._api._transport._host + super().__init__(**kwargs) + self._target = self._transport._host self._closed = False @classmethod - def from_service_account_file(cls, filename: str, **kwargs: Any) -> "Client": + def from_service_account_file( # type: ignore[override] + cls, filename: str, **kwargs: Any + ) -> "Client": """Creates an instance of this client using the provided credentials file. @@ -109,7 +107,7 @@ def from_service_account_file(cls, filename: str, **kwargs: Any) -> "Client": kwargs["credentials"] = credentials return cls(**kwargs) - from_service_account_json = from_service_account_file + from_service_account_json = from_service_account_file # type: ignore[assignment] @property def target(self) -> str: @@ -120,11 +118,6 @@ def target(self) -> str: """ return self._target - @property - def api(self) -> subscriber_client.SubscriberClient: - """The underlying gapic API client.""" - return self._api - @property def closed(self) -> bool: """Return whether the client has been closed and cannot be used anymore. @@ -133,12 +126,32 @@ def closed(self) -> bool: """ return self._closed + @property + def api(self): + """The underlying gapic API client. + + .. versionchanged:: 2.10.0 + Instead of a GAPIC ``SubscriberClient`` client instance, this property is a + proxy object to it with the same interface. + + .. deprecated:: 2.10.0 + Use the GAPIC methods and properties on the client instance directly + instead of through the :attr:`api` attribute. + """ + msg = ( + 'The "api" property only exists for backward compatibility, access its ' + 'attributes directly thorugh the client instance (e.g. "client.foo" ' + 'instead of "client.api.foo").' + ) + warnings.warn(msg, category=DeprecationWarning) + return super() + def subscribe( self, subscription: str, callback: Callable[["subscriber.message.Message"], Any], flow_control: Union[types.FlowControl, Sequence] = (), - scheduler: Optional["subscriber.scheduler.Scheduler"] = None, + scheduler: Optional["subscriber.scheduler.ThreadScheduler"] = None, use_legacy_flow_control: bool = False, await_callbacks_on_shutdown: bool = False, ) -> futures.StreamingPullFuture: @@ -263,7 +276,8 @@ def close(self) -> None: This method is idempotent. """ - self.api._transport.grpc_channel.close() + transport = cast("SubscriberGrpcTransport", self._transport) + transport.grpc_channel.close() self._closed = True def __enter__(self) -> "Client": diff --git a/google/cloud/pubsub_v1/subscriber/exceptions.py b/google/cloud/pubsub_v1/subscriber/exceptions.py new file mode 100644 index 000000000..a5dad31a9 --- /dev/null +++ b/google/cloud/pubsub_v1/subscriber/exceptions.py @@ -0,0 +1,44 @@ +# Copyright 2017, Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +from enum import Enum +from google.api_core.exceptions import GoogleAPICallError +from typing import Optional + + +class AcknowledgeStatus(Enum): + SUCCESS = 1 + PERMISSION_DENIED = 2 + FAILED_PRECONDITION = 3 + INVALID_ACK_ID = 4 + OTHER = 5 + + +class AcknowledgeError(GoogleAPICallError): + """Error during ack/modack/nack operation on exactly-once-enabled subscription.""" + + def __init__(self, error_code: AcknowledgeStatus, info: Optional[str]): + self.error_code = error_code + self.info = info + message = None + if info: + message = str(self.error_code) + " : " + str(self.info) + else: + message = str(self.error_code) + super(AcknowledgeError, self).__init__(message) + + +__all__ = ("AcknowledgeError",) diff --git a/google/cloud/pubsub_v1/subscriber/futures.py b/google/cloud/pubsub_v1/subscriber/futures.py index eec9590ed..f043b7eb5 100644 --- a/google/cloud/pubsub_v1/subscriber/futures.py +++ b/google/cloud/pubsub_v1/subscriber/futures.py @@ -16,9 +16,10 @@ import typing from typing import Any +from typing import Union from google.cloud.pubsub_v1 import futures - +from google.cloud.pubsub_v1.subscriber.exceptions import AcknowledgeStatus if typing.TYPE_CHECKING: # pragma: NO COVER from google.cloud.pubsub_v1.subscriber._protocol.streaming_pull_manager import ( @@ -52,19 +53,27 @@ def _on_close_callback(self, manager: "StreamingPullManager", result: Any): else: self.set_exception(result) - def cancel(self): + def cancel(self) -> bool: """Stops pulling messages and shutdowns the background thread consuming messages. + The method always returns ``True``, as the shutdown is always initiated. + However, if the background stream is already being shut down or the shutdown + has completed, this method is a no-op. + .. versionchanged:: 2.4.1 The method does not block anymore, it just triggers the shutdown and returns immediately. To block until the background stream is terminated, call :meth:`result()` after cancelling the future. + + .. versionchanged:: 2.10.0 + The method always returns ``True`` instead of ``None``. """ # NOTE: We circumvent the base future's self._state to track the cancellation # state, as this state has different meaning with streaming pull futures. self.__cancelled = True - return self.__manager.close() + self.__manager.close() + return True def cancelled(self) -> bool: """ @@ -72,3 +81,45 @@ def cancelled(self) -> bool: ``True`` if the subscription has been cancelled. """ return self.__cancelled + + +class Future(futures.Future): + """This future object is for subscribe-side calls. + + Calling :meth:`result` will resolve the future by returning the message + ID, unless an error occurs. + """ + + def cancel(self) -> bool: + """Actions in Pub/Sub generally may not be canceled. + + This method always returns ``False``. + """ + return False + + def cancelled(self) -> bool: + """Actions in Pub/Sub generally may not be canceled. + + This method always returns ``False``. + """ + return False + + def result(self, timeout: Union[int, float] = None) -> AcknowledgeStatus: + """Return a success code or raise an exception. + + This blocks until the operation completes successfully and + returns the error code unless an exception is raised. + + Args: + timeout: The number of seconds before this call + times out and raises TimeoutError. + + Returns: + AcknowledgeStatus.SUCCESS if the operation succeeded. + + Raises: + concurrent.futures.TimeoutError: If the request times out. + AcknowledgeError: If the operation did not succeed for another + reason. + """ + return super().result(timeout=timeout) diff --git a/google/cloud/pubsub_v1/subscriber/message.py b/google/cloud/pubsub_v1/subscriber/message.py index 4a08257a6..5744aa71c 100644 --- a/google/cloud/pubsub_v1/subscriber/message.py +++ b/google/cloud/pubsub_v1/subscriber/message.py @@ -19,9 +19,12 @@ import math import time import typing -from typing import Optional +from typing import Optional, Callable from google.cloud.pubsub_v1.subscriber._protocol import requests +from google.cloud.pubsub_v1.subscriber import futures +from google.cloud.pubsub_v1.subscriber.exceptions import AcknowledgeStatus + if typing.TYPE_CHECKING: # pragma: NO COVER import datetime @@ -81,12 +84,13 @@ class Message(object): The time that this message was originally published. """ - def __init__( # pytype: disable=module-attr + def __init__( self, - message: "types.PubsubMessage._meta._pb", + message: "types.PubsubMessage._meta._pb", # type: ignore ack_id: str, delivery_attempt: int, request_queue: "queue.Queue", + exactly_once_delivery_enabled_func: Callable[[], bool] = lambda: False, ): """Construct the Message. @@ -110,11 +114,14 @@ def __init__( # pytype: disable=module-attr request_queue: A queue provided by the policy that can accept requests; the policy is responsible for handling those requests. + exactly_once_delivery_enabled_func: + A Callable that returns whether exactly-once delivery is currently-enabled. Defaults to a lambda that always returns False. """ self._message = message self._ack_id = ack_id self._delivery_attempt = delivery_attempt if delivery_attempt > 0 else None self._request_queue = request_queue + self._exactly_once_delivery_enabled_func = exactly_once_delivery_enabled_func self.message_id = message.message_id # The instantiation time is the time that this message @@ -233,7 +240,10 @@ def ack(self) -> None: .. warning:: Acks in Pub/Sub are best effort. You should always ensure that your processing code is idempotent, as you may - receive any given message more than once. + receive any given message more than once. If you need strong + guarantees about acks and re-deliveres, enable exactly-once + delivery on your subscription and use the `ack_with_response` + method instead. """ time_to_ack = math.ceil(time.time() - self._received_timestamp) self._request_queue.put( @@ -242,9 +252,63 @@ def ack(self) -> None: byte_size=self.size, time_to_ack=time_to_ack, ordering_key=self.ordering_key, + future=None, ) ) + def ack_with_response(self) -> "futures.Future": + """Acknowledge the given message. + + Acknowledging a message in Pub/Sub means that you are done + with it, and it will not be delivered to this subscription again. + You should avoid acknowledging messages until you have + *finished* processing them, so that in the event of a failure, + you receive the message again. + + If exactly-once delivery is enabled on the subscription, the + future returned by this method tracks the state of acknowledgement + operation. If the future completes successfully, the message is + guaranteed NOT to be re-delivered. Otherwise, the future will + contain an exception with more details about the failure and the + message may be re-delivered. + + If exactly-once delivery is NOT enabled on the subscription, the + future returns immediately with an AcknowledgeStatus.SUCCESS. + Since acks in Cloud Pub/Sub are best effort when exactly-once + delivery is disabled, the message may be re-delivered. Because + re-deliveries are possible, you should ensure that your processing + code is idempotent, as you may receive any given message more than + once. + + Returns: + A :class:`~google.cloud.pubsub_v1.subscriber.futures.Future` + instance that conforms to Python Standard library's + :class:`~concurrent.futures.Future` interface (but not an + instance of that class). Call `result()` to get the result + of the operation; upon success, a + pubsub_v1.subscriber.exceptions.AcknowledgeStatus.SUCCESS + will be returned and upon an error, an + pubsub_v1.subscriber.exceptions.AcknowledgeError exception + will be thrown. + """ + future = futures.Future() + req_future = None + if self._exactly_once_delivery_enabled_func(): + req_future = future + else: + future.set_result(AcknowledgeStatus.SUCCESS) + time_to_ack = math.ceil(time.time() - self._received_timestamp) + self._request_queue.put( + requests.AckRequest( + ack_id=self._ack_id, + byte_size=self.size, + time_to_ack=time_to_ack, + ordering_key=self.ordering_key, + future=req_future, + ) + ) + return future + def drop(self) -> None: """Release the message from lease management. @@ -269,8 +333,8 @@ def modify_ack_deadline(self, seconds: int) -> None: New deadline will be the given value of seconds from now. - The default implementation handles this for you; you should not need - to manually deal with setting ack deadlines. The exception case is + The default implementation handles automatically modacking received messages for you; + you should not need to manually deal with setting ack deadlines. The exception case is if you are implementing your own custom subclass of :class:`~.pubsub_v1.subcriber._consumer.Consumer`. @@ -281,16 +345,126 @@ def modify_ack_deadline(self, seconds: int) -> None: against. """ self._request_queue.put( - requests.ModAckRequest(ack_id=self._ack_id, seconds=seconds) + requests.ModAckRequest(ack_id=self._ack_id, seconds=seconds, future=None) ) + def modify_ack_deadline_with_response(self, seconds: int) -> "futures.Future": + """Resets the deadline for acknowledgement and returns the response + status via a future. + + New deadline will be the given value of seconds from now. + + The default implementation handles automatically modacking received messages for you; + you should not need to manually deal with setting ack deadlines. The exception case is + if you are implementing your own custom subclass of + :class:`~.pubsub_v1.subcriber._consumer.Consumer`. + + If exactly-once delivery is enabled on the subscription, the + future returned by this method tracks the state of the + modify-ack-deadline operation. If the future completes successfully, + the message is guaranteed NOT to be re-delivered within the new deadline. + Otherwise, the future will contain an exception with more details about + the failure and the message will be redelivered according to its + currently-set ack deadline. + + If exactly-once delivery is NOT enabled on the subscription, the + future returns immediately with an AcknowledgeStatus.SUCCESS. + Since modify-ack-deadline operations in Cloud Pub/Sub are best effort + when exactly-once delivery is disabled, the message may be re-delivered + within the set deadline. + + Args: + seconds: + The number of seconds to set the lease deadline to. This should be + between 0 and 600. Due to network latency, values below 10 are advised + against. + Returns: + A :class:`~google.cloud.pubsub_v1.subscriber.futures.Future` + instance that conforms to Python Standard library's + :class:`~concurrent.futures.Future` interface (but not an + instance of that class). Call `result()` to get the result + of the operation; upon success, a + pubsub_v1.subscriber.exceptions.AcknowledgeStatus.SUCCESS + will be returned and upon an error, an + pubsub_v1.subscriber.exceptions.AcknowledgeError exception + will be thrown. + + """ + future = futures.Future() + req_future = None + if self._exactly_once_delivery_enabled_func(): + req_future = future + else: + future.set_result(AcknowledgeStatus.SUCCESS) + + self._request_queue.put( + requests.ModAckRequest( + ack_id=self._ack_id, seconds=seconds, future=req_future + ) + ) + + return future + def nack(self) -> None: - """Decline to acknowldge the given message. + """Decline to acknowledge the given message. - This will cause the message to be re-delivered to the subscription. + This will cause the message to be re-delivered to subscribers. Re-deliveries + may take place immediately or after a delay, and may arrive at this subscriber + or another. """ self._request_queue.put( requests.NackRequest( - ack_id=self._ack_id, byte_size=self.size, ordering_key=self.ordering_key + ack_id=self._ack_id, + byte_size=self.size, + ordering_key=self.ordering_key, + future=None, + ) + ) + + def nack_with_response(self) -> "futures.Future": + """Decline to acknowledge the given message, returning the response status via + a future. + + This will cause the message to be re-delivered to subscribers. Re-deliveries + may take place immediately or after a delay, and may arrive at this subscriber + or another. + + If exactly-once delivery is enabled on the subscription, the + future returned by this method tracks the state of the + nack operation. If the future completes successfully, + the future's result will be an AcknowledgeStatus.SUCCESS. + Otherwise, the future will contain an exception with more details about + the failure. + + If exactly-once delivery is NOT enabled on the subscription, the + future returns immediately with an AcknowledgeStatus.SUCCESS. + + Returns: + A :class:`~google.cloud.pubsub_v1.subscriber.futures.Future` + instance that conforms to Python Standard library's + :class:`~concurrent.futures.Future` interface (but not an + instance of that class). Call `result()` to get the result + of the operation; upon success, a + pubsub_v1.subscriber.exceptions.AcknowledgeStatus.SUCCESS + will be returned and upon an error, an + pubsub_v1.subscriber.exceptions.AcknowledgeError exception + will be thrown. + + """ + future = futures.Future() + req_future = None + if self._exactly_once_delivery_enabled_func(): + req_future = future + else: + future.set_result(AcknowledgeStatus.SUCCESS) + + self._request_queue.put( + requests.NackRequest( + ack_id=self._ack_id, + byte_size=self.size, + ordering_key=self.ordering_key, + future=req_future, ) ) + + return future diff --git a/google/cloud/pubsub_v1/subscriber/scheduler.py b/google/cloud/pubsub_v1/subscriber/scheduler.py index 3db7ed73e..ca270a077 100644 --- a/google/cloud/pubsub_v1/subscriber/scheduler.py +++ b/google/cloud/pubsub_v1/subscriber/scheduler.py @@ -101,7 +101,7 @@ class ThreadScheduler(Scheduler): def __init__( self, executor: Optional[concurrent.futures.ThreadPoolExecutor] = None ): - self._queue = queue.Queue() + self._queue: queue.Queue = queue.Queue() if executor is None: self._executor = _make_default_thread_pool_executor() else: diff --git a/google/cloud/pubsub_v1/types.py b/google/cloud/pubsub_v1/types.py index 0558c2f1e..109d4aadc 100644 --- a/google/cloud/pubsub_v1/types.py +++ b/google/cloud/pubsub_v1/types.py @@ -19,15 +19,15 @@ import inspect import sys import typing -from typing import Dict, NamedTuple +from typing import Dict, NamedTuple, Union -import proto +import proto # type: ignore -from google.api import http_pb2 +from google.api import http_pb2 # type: ignore from google.api_core import gapic_v1 -from google.iam.v1 import iam_policy_pb2 +from google.iam.v1 import iam_policy_pb2 # type: ignore from google.iam.v1 import policy_pb2 -from google.iam.v1.logging import audit_data_pb2 +from google.iam.v1.logging import audit_data_pb2 # type: ignore from google.protobuf import descriptor_pb2 from google.protobuf import duration_pb2 from google.protobuf import empty_pb2 @@ -41,8 +41,18 @@ if typing.TYPE_CHECKING: # pragma: NO COVER from types import ModuleType - from google.api_core import retry as retries from google.pubsub_v1 import types as gapic_types + from google.pubsub_v1.services.publisher.client import OptionalRetry + + # TODO: Eventually implement OptionalTimeout in the GAPIC code generator and import + # it from the generated code. It's the same solution that is used for OptionalRetry. + # https://github.com/googleapis/gapic-generator-python/pull/1032/files + # https://github.com/googleapis/gapic-generator-python/pull/1065/files + if hasattr(gapic_v1.method, "_MethodDefault"): + # _MethodDefault was only added in google-api-core==2.2.2 + OptionalTimeout = Union[gapic_types.TimeoutType, gapic_v1.method._MethodDefault] + else: + OptionalTimeout = Union[gapic_types.TimeoutType, object] # type: ignore # Define the default values for batching. @@ -85,10 +95,10 @@ class LimitExceededBehavior(str, enum.Enum): class PublishFlowControl(NamedTuple): """The client flow control settings for message publishing.""" - message_limit: int = 10 * BatchSettings.__new__.__defaults__[2] + message_limit: int = 10 * BatchSettings.__new__.__defaults__[2] # type: ignore """The maximum number of messages awaiting to be published.""" - byte_limit: int = 10 * BatchSettings.__new__.__defaults__[0] + byte_limit: int = 10 * BatchSettings.__new__.__defaults__[0] # type: ignore """The maximum total size of messages awaiting to be published.""" limit_exceeded_behavior: LimitExceededBehavior = LimitExceededBehavior.IGNORE @@ -111,13 +121,13 @@ class PublisherOptions(NamedTuple): "the publisher client does not do any throttling." ) - retry: "retries.Retry" = gapic_v1.method.DEFAULT # use api_core default + retry: "OptionalRetry" = gapic_v1.method.DEFAULT # use api_core default ( "Retry settings for message publishing by the client. This should be " "an instance of :class:`google.api_core.retry.Retry`." ) - timeout: "gapic_types.TimeoutType" = gapic_v1.method.DEFAULT # use api_core default + timeout: "OptionalTimeout" = gapic_v1.method.DEFAULT # use api_core default ( "Timeout settings for message publishing by the client. It should be " "compatible with :class:`~.pubsub_v1.types.TimeoutType`." @@ -152,6 +162,13 @@ class FlowControl(NamedTuple): "before dropping it from the lease management." ) + min_duration_per_lease_extension: float = 0 + ( + "The min amount of time in seconds for a single lease extension attempt. " + "Must be between 10 and 600 (inclusive). Ignored by default, but set to " + "60 seconds if the subscription has exactly-once delivery enabled." + ) + max_duration_per_lease_extension: float = 0 # disabled by default ( "The max amount of time in seconds for a single lease extension attempt. " diff --git a/google/pubsub/__init__.py b/google/pubsub/__init__.py index dfa5c7e04..9fc4e6feb 100644 --- a/google/pubsub/__init__.py +++ b/google/pubsub/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub/py.typed b/google/pubsub/py.typed index 9b0e37433..1cec9a5ba 100644 --- a/google/pubsub/py.typed +++ b/google/pubsub/py.typed @@ -1,2 +1,2 @@ # Marker file for PEP 561. -# The google-pubsub package uses inline types. +# The google-cloud-pubsub package uses inline types. diff --git a/google/pubsub_v1/__init__.py b/google/pubsub_v1/__init__.py index bc78db26f..75c41bf21 100644 --- a/google/pubsub_v1/__init__.py +++ b/google/pubsub_v1/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/py.typed b/google/pubsub_v1/py.typed index 9b0e37433..1cec9a5ba 100644 --- a/google/pubsub_v1/py.typed +++ b/google/pubsub_v1/py.typed @@ -1,2 +1,2 @@ # Marker file for PEP 561. -# The google-pubsub package uses inline types. +# The google-cloud-pubsub package uses inline types. diff --git a/google/pubsub_v1/services/__init__.py b/google/pubsub_v1/services/__init__.py index 4de65971c..e8e1c3845 100644 --- a/google/pubsub_v1/services/__init__.py +++ b/google/pubsub_v1/services/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/publisher/__init__.py b/google/pubsub_v1/services/publisher/__init__.py index 98e50425d..56fb64a17 100644 --- a/google/pubsub_v1/services/publisher/__init__.py +++ b/google/pubsub_v1/services/publisher/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/publisher/async_client.py b/google/pubsub_v1/services/publisher/async_client.py index 7165061d2..b818e6cbb 100644 --- a/google/pubsub_v1/services/publisher/async_client.py +++ b/google/pubsub_v1/services/publisher/async_client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ from collections import OrderedDict import functools import re -from typing import Dict, Sequence, Tuple, Type, Union +from typing import Dict, Optional, Sequence, Tuple, Type, Union import pkg_resources from google.api_core.client_options import ClientOptions @@ -111,6 +111,42 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return PublisherClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + @property def transport(self) -> PublisherTransport: """Returns the transport used by the client instance. @@ -184,6 +220,26 @@ async def create_topic( name rules] (https://cloud.google.com/pubsub/docs/admin#resource_names). + + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.Topic( + name="name_value", + ) + + # Make the request + response = client.create_topic(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.Topic, dict]): The request object. A topic resource. @@ -212,7 +268,7 @@ async def create_topic( A topic resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name]) if request is not None and has_flattened_params: @@ -268,6 +324,29 @@ async def update_topic( r"""Updates an existing topic. Note that certain properties of a topic are not modifiable. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_update_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + topic = pubsub_v1.Topic() + topic.name = "name_value" + + request = pubsub_v1.UpdateTopicRequest( + topic=topic, + ) + + # Make the request + response = client.update_topic(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.UpdateTopicRequest, dict]): The request object. Request for the UpdateTopic method. @@ -329,6 +408,26 @@ async def publish( r"""Adds one or more messages to the topic. Returns ``NOT_FOUND`` if the topic does not exist. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_publish(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.PublishRequest( + topic="topic_value", + ) + + # Make the request + response = client.publish(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.PublishRequest, dict]): The request object. Request for the Publish method. @@ -357,7 +456,7 @@ async def publish( Response for the Publish method. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic, messages]) if request is not None and has_flattened_params: @@ -421,6 +520,25 @@ async def get_topic( ) -> pubsub.Topic: r"""Gets the configuration of a topic. + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.GetTopicRequest( + topic="topic_value", + ) + + # Make the request + response = client.get_topic(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.GetTopicRequest, dict]): The request object. Request for the GetTopic method. @@ -443,7 +561,7 @@ async def get_topic( A topic resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -501,6 +619,26 @@ async def list_topics( ) -> pagers.ListTopicsAsyncPager: r"""Lists matching topics. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_topics(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_topics(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListTopicsRequest, dict]): The request object. Request for the `ListTopics` method. @@ -527,7 +665,7 @@ async def list_topics( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([project]) if request is not None and has_flattened_params: @@ -592,6 +730,27 @@ async def list_topic_subscriptions( r"""Lists the names of the attached subscriptions on this topic. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_topic_subscriptions(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSubscriptionsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_subscriptions(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListTopicSubscriptionsRequest, dict]): The request object. Request for the @@ -620,7 +779,7 @@ async def list_topic_subscriptions( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -689,6 +848,27 @@ async def list_topic_snapshots( bulk. That is, you can set the acknowledgment state of messages in an existing subscription to the state captured by a snapshot. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_topic_snapshots(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSnapshotsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_snapshots(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListTopicSnapshotsRequest, dict]): The request object. Request for the `ListTopicSnapshots` @@ -717,7 +897,7 @@ async def list_topic_snapshots( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -786,6 +966,23 @@ async def delete_topic( subscriptions to this topic are not deleted, but their ``topic`` field is set to ``_deleted-topic_``. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteTopicRequest( + topic="topic_value", + ) + + # Make the request + client.delete_topic(request=request) + Args: request (Union[google.pubsub_v1.types.DeleteTopicRequest, dict]): The request object. Request for the `DeleteTopic` @@ -805,7 +1002,7 @@ async def delete_topic( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -863,6 +1060,26 @@ async def detach_subscription( the subscription is a push subscription, pushes to the endpoint will stop. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_detach_subscription(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.DetachSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.detach_subscription(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.DetachSubscriptionRequest, dict]): The request object. Request for the DetachSubscription diff --git a/google/pubsub_v1/services/publisher/client.py b/google/pubsub_v1/services/publisher/client.py index 51cc2d1ad..e4eb074c5 100644 --- a/google/pubsub_v1/services/publisher/client.py +++ b/google/pubsub_v1/services/publisher/client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -279,6 +279,74 @@ def parse_common_location_path(path: str) -> Dict[str, str]: m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) return m.groupdict() if m else {} + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_client_cert not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert == "true": + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + def __init__( self, *, @@ -330,57 +398,22 @@ def __init__( if client_options is None: client_options = client_options_lib.ClientOptions() - # Create SSL credentials for mutual TLS if needed. - if os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") not in ( - "true", - "false", - ): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true" + api_endpoint, client_cert_source_func = self.get_mtls_endpoint_and_cert_source( + client_options ) - client_cert_source_func = None - is_mtls = False - if use_client_cert: - if client_options.client_cert_source: - is_mtls = True - client_cert_source_func = client_options.client_cert_source - else: - is_mtls = mtls.has_default_client_cert_source() - if is_mtls: - client_cert_source_func = mtls.default_client_cert_source() - else: - client_cert_source_func = None - - # Figure out which api endpoint to use. - if client_options.api_endpoint is not None: - api_endpoint = client_options.api_endpoint - else: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_mtls_env == "never": - api_endpoint = self.DEFAULT_ENDPOINT - elif use_mtls_env == "always": - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - elif use_mtls_env == "auto": - if is_mtls: - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = self.DEFAULT_ENDPOINT - else: - raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " - "values: never, auto, always" - ) + api_key_value = getattr(client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) # Save or instantiate the transport. # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, PublisherTransport): # transport is a PublisherTransport instance. - if credentials or client_options.credentials_file: + if credentials or client_options.credentials_file or api_key_value: raise ValueError( "When providing a transport instance, " "provide its credentials directly." @@ -392,6 +425,15 @@ def __init__( ) self._transport = transport else: + import google.auth._default # type: ignore + + if api_key_value and hasattr( + google.auth._default, "get_api_key_credentials" + ): + credentials = google.auth._default.get_api_key_credentials( + api_key_value + ) + Transport = type(self).get_transport_class(transport) emulator_host = os.environ.get("PUBSUB_EMULATOR_HOST") @@ -427,6 +469,26 @@ def create_topic( (https://cloud.google.com/pubsub/docs/admin#resource_names). + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.Topic( + name="name_value", + ) + + # Make the request + response = client.create_topic(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.Topic, dict]): The request object. A topic resource. @@ -455,7 +517,7 @@ def create_topic( A topic resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name]) if request is not None and has_flattened_params: @@ -503,6 +565,29 @@ def update_topic( properties of a topic are not modifiable. + .. code-block:: python + + from google import pubsub_v1 + + def sample_update_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + topic = pubsub_v1.Topic() + topic.name = "name_value" + + request = pubsub_v1.UpdateTopicRequest( + topic=topic, + ) + + # Make the request + response = client.update_topic(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.UpdateTopicRequest, dict]): The request object. Request for the UpdateTopic method. @@ -557,6 +642,26 @@ def publish( the topic does not exist. + .. code-block:: python + + from google import pubsub_v1 + + def sample_publish(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.PublishRequest( + topic="topic_value", + ) + + # Make the request + response = client.publish(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.PublishRequest, dict]): The request object. Request for the Publish method. @@ -585,7 +690,7 @@ def publish( Response for the Publish method. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic, messages]) if request is not None and has_flattened_params: @@ -634,6 +739,25 @@ def get_topic( ) -> pubsub.Topic: r"""Gets the configuration of a topic. + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.GetTopicRequest( + topic="topic_value", + ) + + # Make the request + response = client.get_topic(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.GetTopicRequest, dict]): @@ -657,7 +781,7 @@ def get_topic( A topic resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -704,6 +828,26 @@ def list_topics( ) -> pagers.ListTopicsPager: r"""Lists matching topics. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_topics(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_topics(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListTopicsRequest, dict]): @@ -731,7 +875,7 @@ def list_topics( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([project]) if request is not None and has_flattened_params: @@ -786,6 +930,27 @@ def list_topic_subscriptions( topic. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_topic_subscriptions(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSubscriptionsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_subscriptions(request=request) + + # Handle the response + for response in page_result: + print(response) + + Args: request (Union[google.pubsub_v1.types.ListTopicSubscriptionsRequest, dict]): The request object. Request for the @@ -814,7 +979,7 @@ def list_topic_subscriptions( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -873,6 +1038,27 @@ def list_topic_snapshots( in an existing subscription to the state captured by a snapshot. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_topic_snapshots(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSnapshotsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_snapshots(request=request) + + # Handle the response + for response in page_result: + print(response) + + Args: request (Union[google.pubsub_v1.types.ListTopicSnapshotsRequest, dict]): The request object. Request for the `ListTopicSnapshots` @@ -901,7 +1087,7 @@ def list_topic_snapshots( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -960,6 +1146,23 @@ def delete_topic( field is set to ``_deleted-topic_``. + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteTopicRequest( + topic="topic_value", + ) + + # Make the request + client.delete_topic(request=request) + + Args: request (Union[google.pubsub_v1.types.DeleteTopicRequest, dict]): The request object. Request for the `DeleteTopic` @@ -979,7 +1182,7 @@ def delete_topic( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([topic]) if request is not None and has_flattened_params: @@ -1029,6 +1232,26 @@ def detach_subscription( will stop. + .. code-block:: python + + from google import pubsub_v1 + + def sample_detach_subscription(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.DetachSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.detach_subscription(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.DetachSubscriptionRequest, dict]): The request object. Request for the DetachSubscription diff --git a/google/pubsub_v1/services/publisher/pagers.py b/google/pubsub_v1/services/publisher/pagers.py index 50096517d..1e095181f 100644 --- a/google/pubsub_v1/services/publisher/pagers.py +++ b/google/pubsub_v1/services/publisher/pagers.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/publisher/transports/__init__.py b/google/pubsub_v1/services/publisher/transports/__init__.py index 34066edc3..e73fe8901 100644 --- a/google/pubsub_v1/services/publisher/transports/__init__.py +++ b/google/pubsub_v1/services/publisher/transports/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/publisher/transports/base.py b/google/pubsub_v1/services/publisher/transports/base.py index bdb55ab7b..a3ea34af6 100644 --- a/google/pubsub_v1/services/publisher/transports/base.py +++ b/google/pubsub_v1/services/publisher/transports/base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -107,7 +107,6 @@ def __init__( credentials, _ = google.auth.load_credentials_from_file( credentials_file, **scopes_kwargs, quota_project_id=quota_project_id ) - elif credentials is None: credentials, _ = google.auth.default( **scopes_kwargs, quota_project_id=quota_project_id diff --git a/google/pubsub_v1/services/publisher/transports/grpc.py b/google/pubsub_v1/services/publisher/transports/grpc.py index 2507d9742..43fae36d6 100644 --- a/google/pubsub_v1/services/publisher/transports/grpc.py +++ b/google/pubsub_v1/services/publisher/transports/grpc.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -162,8 +162,11 @@ def __init__( if not self._grpc_channel: self._grpc_channel = type(self).create_channel( self._host, + # use the credentials which are saved credentials=self._credentials, - credentials_file=credentials_file, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, diff --git a/google/pubsub_v1/services/publisher/transports/grpc_asyncio.py b/google/pubsub_v1/services/publisher/transports/grpc_asyncio.py index 26677ad12..3729275f9 100644 --- a/google/pubsub_v1/services/publisher/transports/grpc_asyncio.py +++ b/google/pubsub_v1/services/publisher/transports/grpc_asyncio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -207,8 +207,11 @@ def __init__( if not self._grpc_channel: self._grpc_channel = type(self).create_channel( self._host, + # use the credentials which are saved credentials=self._credentials, - credentials_file=credentials_file, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, diff --git a/google/pubsub_v1/services/schema_service/__init__.py b/google/pubsub_v1/services/schema_service/__init__.py index 523d5b5f5..2609e9ecd 100644 --- a/google/pubsub_v1/services/schema_service/__init__.py +++ b/google/pubsub_v1/services/schema_service/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/schema_service/async_client.py b/google/pubsub_v1/services/schema_service/async_client.py index 64f65ae07..17c2bdaa6 100644 --- a/google/pubsub_v1/services/schema_service/async_client.py +++ b/google/pubsub_v1/services/schema_service/async_client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ from collections import OrderedDict import functools import re -from typing import Dict, Sequence, Tuple, Type, Union +from typing import Dict, Optional, Sequence, Tuple, Type, Union import pkg_resources from google.api_core.client_options import ClientOptions @@ -109,6 +109,42 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return SchemaServiceClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + @property def transport(self) -> SchemaServiceTransport: """Returns the transport used by the client instance. @@ -182,6 +218,29 @@ async def create_schema( ) -> gp_schema.Schema: r"""Creates a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.CreateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = client.create_schema(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.CreateSchemaRequest, dict]): The request object. Request for the CreateSchema method. @@ -224,7 +283,7 @@ async def create_schema( A schema resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([parent, schema, schema_id]) if request is not None and has_flattened_params: @@ -275,6 +334,25 @@ async def get_schema( ) -> schema.Schema: r"""Gets a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSchemaRequest( + name="name_value", + ) + + # Make the request + response = client.get_schema(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.GetSchemaRequest, dict]): The request object. Request for the GetSchema method. @@ -296,7 +374,7 @@ async def get_schema( A schema resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name]) if request is not None and has_flattened_params: @@ -343,6 +421,26 @@ async def list_schemas( ) -> pagers.ListSchemasAsyncPager: r"""Lists schemas in a project. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_schemas(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSchemasRequest( + parent="parent_value", + ) + + # Make the request + page_result = client.list_schemas(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListSchemasRequest, dict]): The request object. Request for the `ListSchemas` @@ -369,7 +467,7 @@ async def list_schemas( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([parent]) if request is not None and has_flattened_params: @@ -422,6 +520,22 @@ async def delete_schema( ) -> None: r"""Deletes a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSchemaRequest( + name="name_value", + ) + + # Make the request + client.delete_schema(request=request) + Args: request (Union[google.pubsub_v1.types.DeleteSchemaRequest, dict]): The request object. Request for the `DeleteSchema` @@ -440,7 +554,7 @@ async def delete_schema( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name]) if request is not None and has_flattened_params: @@ -487,6 +601,29 @@ async def validate_schema( ) -> gp_schema.ValidateSchemaResponse: r"""Validates a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_validate_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.ValidateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = client.validate_schema(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.ValidateSchemaRequest, dict]): The request object. Request for the `ValidateSchema` @@ -518,7 +655,7 @@ async def validate_schema( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([parent, schema]) if request is not None and has_flattened_params: @@ -566,6 +703,26 @@ async def validate_message( ) -> schema.ValidateMessageResponse: r"""Validates a message against a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_validate_message(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.ValidateMessageRequest( + name="name_value", + parent="parent_value", + ) + + # Make the request + response = client.validate_message(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.ValidateMessageRequest, dict]): The request object. Request for the `ValidateMessage` diff --git a/google/pubsub_v1/services/schema_service/client.py b/google/pubsub_v1/services/schema_service/client.py index ccb958dd8..825aa3998 100644 --- a/google/pubsub_v1/services/schema_service/client.py +++ b/google/pubsub_v1/services/schema_service/client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -232,6 +232,73 @@ def parse_common_location_path(path: str) -> Dict[str, str]: m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) return m.groupdict() if m else {} + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_client_cert not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert == "true": + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + def __init__( self, *, @@ -282,57 +349,22 @@ def __init__( if client_options is None: client_options = client_options_lib.ClientOptions() - # Create SSL credentials for mutual TLS if needed. - if os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") not in ( - "true", - "false", - ): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true" + api_endpoint, client_cert_source_func = self.get_mtls_endpoint_and_cert_source( + client_options ) - client_cert_source_func = None - is_mtls = False - if use_client_cert: - if client_options.client_cert_source: - is_mtls = True - client_cert_source_func = client_options.client_cert_source - else: - is_mtls = mtls.has_default_client_cert_source() - if is_mtls: - client_cert_source_func = mtls.default_client_cert_source() - else: - client_cert_source_func = None - - # Figure out which api endpoint to use. - if client_options.api_endpoint is not None: - api_endpoint = client_options.api_endpoint - else: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_mtls_env == "never": - api_endpoint = self.DEFAULT_ENDPOINT - elif use_mtls_env == "always": - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - elif use_mtls_env == "auto": - if is_mtls: - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = self.DEFAULT_ENDPOINT - else: - raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " - "values: never, auto, always" - ) + api_key_value = getattr(client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) # Save or instantiate the transport. # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, SchemaServiceTransport): # transport is a SchemaServiceTransport instance. - if credentials or client_options.credentials_file: + if credentials or client_options.credentials_file or api_key_value: raise ValueError( "When providing a transport instance, " "provide its credentials directly." @@ -344,6 +376,15 @@ def __init__( ) self._transport = transport else: + import google.auth._default # type: ignore + + if api_key_value and hasattr( + google.auth._default, "get_api_key_credentials" + ): + credentials = google.auth._default.get_api_key_credentials( + api_key_value + ) + Transport = type(self).get_transport_class(transport) self._transport = Transport( credentials=credentials, @@ -369,6 +410,29 @@ def create_schema( ) -> gp_schema.Schema: r"""Creates a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.CreateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = client.create_schema(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.CreateSchemaRequest, dict]): The request object. Request for the CreateSchema method. @@ -411,7 +475,7 @@ def create_schema( A schema resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([parent, schema, schema_id]) if request is not None and has_flattened_params: @@ -462,6 +526,25 @@ def get_schema( ) -> schema.Schema: r"""Gets a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSchemaRequest( + name="name_value", + ) + + # Make the request + response = client.get_schema(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.GetSchemaRequest, dict]): The request object. Request for the GetSchema method. @@ -483,7 +566,7 @@ def get_schema( A schema resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name]) if request is not None and has_flattened_params: @@ -530,6 +613,26 @@ def list_schemas( ) -> pagers.ListSchemasPager: r"""Lists schemas in a project. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_schemas(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSchemasRequest( + parent="parent_value", + ) + + # Make the request + page_result = client.list_schemas(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListSchemasRequest, dict]): The request object. Request for the `ListSchemas` @@ -556,7 +659,7 @@ def list_schemas( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([parent]) if request is not None and has_flattened_params: @@ -609,6 +712,22 @@ def delete_schema( ) -> None: r"""Deletes a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSchemaRequest( + name="name_value", + ) + + # Make the request + client.delete_schema(request=request) + Args: request (Union[google.pubsub_v1.types.DeleteSchemaRequest, dict]): The request object. Request for the `DeleteSchema` @@ -627,7 +746,7 @@ def delete_schema( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name]) if request is not None and has_flattened_params: @@ -674,6 +793,29 @@ def validate_schema( ) -> gp_schema.ValidateSchemaResponse: r"""Validates a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_validate_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.ValidateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = client.validate_schema(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.ValidateSchemaRequest, dict]): The request object. Request for the `ValidateSchema` @@ -705,7 +847,7 @@ def validate_schema( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([parent, schema]) if request is not None and has_flattened_params: @@ -753,6 +895,26 @@ def validate_message( ) -> schema.ValidateMessageResponse: r"""Validates a message against a schema. + .. code-block:: python + + from google import pubsub_v1 + + def sample_validate_message(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.ValidateMessageRequest( + name="name_value", + parent="parent_value", + ) + + # Make the request + response = client.validate_message(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.ValidateMessageRequest, dict]): The request object. Request for the `ValidateMessage` diff --git a/google/pubsub_v1/services/schema_service/pagers.py b/google/pubsub_v1/services/schema_service/pagers.py index 43d520a26..965778d45 100644 --- a/google/pubsub_v1/services/schema_service/pagers.py +++ b/google/pubsub_v1/services/schema_service/pagers.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/schema_service/transports/__init__.py b/google/pubsub_v1/services/schema_service/transports/__init__.py index 81ebf8d1c..6c2d9460a 100644 --- a/google/pubsub_v1/services/schema_service/transports/__init__.py +++ b/google/pubsub_v1/services/schema_service/transports/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/schema_service/transports/base.py b/google/pubsub_v1/services/schema_service/transports/base.py index 16d98a8c4..97b85afa7 100644 --- a/google/pubsub_v1/services/schema_service/transports/base.py +++ b/google/pubsub_v1/services/schema_service/transports/base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -108,7 +108,6 @@ def __init__( credentials, _ = google.auth.load_credentials_from_file( credentials_file, **scopes_kwargs, quota_project_id=quota_project_id ) - elif credentials is None: credentials, _ = google.auth.default( **scopes_kwargs, quota_project_id=quota_project_id diff --git a/google/pubsub_v1/services/schema_service/transports/grpc.py b/google/pubsub_v1/services/schema_service/transports/grpc.py index 7313ec2dd..6d5290e0d 100644 --- a/google/pubsub_v1/services/schema_service/transports/grpc.py +++ b/google/pubsub_v1/services/schema_service/transports/grpc.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -162,8 +162,11 @@ def __init__( if not self._grpc_channel: self._grpc_channel = type(self).create_channel( self._host, + # use the credentials which are saved credentials=self._credentials, - credentials_file=credentials_file, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, diff --git a/google/pubsub_v1/services/schema_service/transports/grpc_asyncio.py b/google/pubsub_v1/services/schema_service/transports/grpc_asyncio.py index a542e066d..206b0eb26 100644 --- a/google/pubsub_v1/services/schema_service/transports/grpc_asyncio.py +++ b/google/pubsub_v1/services/schema_service/transports/grpc_asyncio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -207,8 +207,11 @@ def __init__( if not self._grpc_channel: self._grpc_channel = type(self).create_channel( self._host, + # use the credentials which are saved credentials=self._credentials, - credentials_file=credentials_file, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, diff --git a/google/pubsub_v1/services/subscriber/__init__.py b/google/pubsub_v1/services/subscriber/__init__.py index 0961d69d1..1d7599467 100644 --- a/google/pubsub_v1/services/subscriber/__init__.py +++ b/google/pubsub_v1/services/subscriber/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/subscriber/async_client.py b/google/pubsub_v1/services/subscriber/async_client.py index 3754f151a..265c7dc75 100644 --- a/google/pubsub_v1/services/subscriber/async_client.py +++ b/google/pubsub_v1/services/subscriber/async_client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import re from typing import ( Dict, + Optional, AsyncIterable, Awaitable, AsyncIterator, @@ -122,6 +123,42 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return SubscriberClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + @property def transport(self) -> SubscriberTransport: """Returns the transport used by the client instance. @@ -208,6 +245,27 @@ async def create_subscription( Note that for REST API requests, you must specify a name in the request. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.Subscription( + name="name_value", + topic="topic_value", + ) + + # Make the request + response = client.create_subscription(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.Subscription, dict]): The request object. A subscription resource. @@ -284,7 +342,7 @@ async def create_subscription( A subscription resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name, topic, push_config, ack_deadline_seconds]) if request is not None and has_flattened_params: @@ -348,6 +406,25 @@ async def get_subscription( ) -> pubsub.Subscription: r"""Gets the configuration details of a subscription. + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.get_subscription(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.GetSubscriptionRequest, dict]): The request object. Request for the GetSubscription @@ -370,7 +447,7 @@ async def get_subscription( A subscription resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription]) if request is not None and has_flattened_params: @@ -431,6 +508,30 @@ async def update_subscription( properties of a subscription, such as its topic, are not modifiable. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_update_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + subscription = pubsub_v1.Subscription() + subscription.name = "name_value" + subscription.topic = "topic_value" + + request = pubsub_v1.UpdateSubscriptionRequest( + subscription=subscription, + ) + + # Make the request + response = client.update_subscription(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.UpdateSubscriptionRequest, dict]): The request object. Request for the UpdateSubscription @@ -490,6 +591,26 @@ async def list_subscriptions( ) -> pagers.ListSubscriptionsAsyncPager: r"""Lists matching subscriptions. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_subscriptions(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSubscriptionsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_subscriptions(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListSubscriptionsRequest, dict]): The request object. Request for the `ListSubscriptions` @@ -516,7 +637,7 @@ async def list_subscriptions( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([project]) if request is not None and has_flattened_params: @@ -585,6 +706,23 @@ async def delete_subscription( new one has no association with the old subscription or its topic unless the same topic is specified. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + client.delete_subscription(request=request) + Args: request (Union[google.pubsub_v1.types.DeleteSubscriptionRequest, dict]): The request object. Request for the DeleteSubscription @@ -603,7 +741,7 @@ async def delete_subscription( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription]) if request is not None and has_flattened_params: @@ -667,6 +805,25 @@ async def modify_ack_deadline( does not modify the subscription-level ``ackDeadlineSeconds`` used for subsequent messages. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_modify_ack_deadline(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyAckDeadlineRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ack_deadline_seconds=2066, + ) + + # Make the request + client.modify_ack_deadline(request=request) + Args: request (Union[google.pubsub_v1.types.ModifyAckDeadlineRequest, dict]): The request object. Request for the ModifyAckDeadline @@ -706,7 +863,7 @@ async def modify_ack_deadline( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, ack_ids, ack_deadline_seconds]) if request is not None and has_flattened_params: @@ -775,6 +932,24 @@ async def acknowledge( Acknowledging a message more than once will not result in an error. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_acknowledge(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.AcknowledgeRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ) + + # Make the request + client.acknowledge(request=request) + Args: request (Union[google.pubsub_v1.types.AcknowledgeRequest, dict]): The request object. Request for the Acknowledge method. @@ -801,7 +976,7 @@ async def acknowledge( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, ack_ids]) if request is not None and has_flattened_params: @@ -864,6 +1039,27 @@ async def pull( ``UNAVAILABLE`` if there are too many concurrent pull requests pending for the given subscription. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_pull(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.PullRequest( + subscription="subscription_value", + max_messages=1277, + ) + + # Make the request + response = client.pull(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.PullRequest, dict]): The request object. Request for the `Pull` method. @@ -910,7 +1106,7 @@ async def pull( Response for the Pull method. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, return_immediately, max_messages]) if request is not None and has_flattened_params: @@ -986,6 +1182,38 @@ def streaming_pull( re-establish the stream. Flow control can be achieved by configuring the underlying RPC channel. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_streaming_pull(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.StreamingPullRequest( + subscription="subscription_value", + stream_ack_deadline_seconds=2813, + ) + + # This method expects an iterator which contains + # 'pubsub_v1.StreamingPullRequest' objects + # Here we create a generator that yields a single `request` for + # demonstrative purposes. + requests = [request] + + def request_generator(): + for request in requests: + yield request + + # Make the request + stream = client.streaming_pull(requests=request_generator()) + + # Handle the response + for response in stream: + print(response) + Args: requests (AsyncIterator[`google.pubsub_v1.types.StreamingPullRequest`]): The request object AsyncIterator. Request for the `StreamingPull` @@ -1051,6 +1279,23 @@ async def modify_push_config( Messages will accumulate for delivery continuously through the call regardless of changes to the ``PushConfig``. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_modify_push_config(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyPushConfigRequest( + subscription="subscription_value", + ) + + # Make the request + client.modify_push_config(request=request) + Args: request (Union[google.pubsub_v1.types.ModifyPushConfigRequest, dict]): The request object. Request for the ModifyPushConfig @@ -1081,7 +1326,7 @@ async def modify_push_config( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, push_config]) if request is not None and has_flattened_params: @@ -1140,12 +1385,32 @@ async def get_snapshot( ) -> pubsub.Snapshot: r"""Gets the configuration details of a snapshot. Snapshots are used in Seek operations, which allow you to manage - message acknowledgments in bulk. That is, you can set - the acknowledgment state of messages in an existing + href="https://cloud.google.com/pubsub/docs/replay-overview">Seek + operations, which allow you to manage message + acknowledgments in bulk. That is, you can set the + acknowledgment state of messages in an existing subscription to the state captured by a snapshot. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + response = client.get_snapshot(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.GetSnapshotRequest, dict]): The request object. Request for the GetSnapshot method. @@ -1173,7 +1438,7 @@ async def get_snapshot( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([snapshot]) if request is not None and has_flattened_params: @@ -1235,6 +1500,27 @@ async def list_snapshots( bulk. That is, you can set the acknowledgment state of messages in an existing subscription to the state captured by a snapshot. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_snapshots(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSnapshotsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_snapshots(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListSnapshotsRequest, dict]): The request object. Request for the `ListSnapshots` @@ -1261,7 +1547,7 @@ async def list_snapshots( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([project]) if request is not None and has_flattened_params: @@ -1344,6 +1630,27 @@ async def create_snapshot( Note that for REST API requests, you must specify a name in the request. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.CreateSnapshotRequest( + name="name_value", + subscription="subscription_value", + ) + + # Make the request + response = client.create_snapshot(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.CreateSnapshotRequest, dict]): The request object. Request for the `CreateSnapshot` @@ -1392,7 +1699,7 @@ async def create_snapshot( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name, subscription]) if request is not None and has_flattened_params: @@ -1448,13 +1755,33 @@ async def update_snapshot( metadata: Sequence[Tuple[str, str]] = (), ) -> pubsub.Snapshot: r"""Updates an existing snapshot. Snapshots are used in - Seek operations, which allow + Seek + operations, which allow you to manage message acknowledgments in bulk. That is, you can set the acknowledgment state of messages in an existing subscription to the state captured by a snapshot. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_update_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.UpdateSnapshotRequest( + ) + + # Make the request + response = client.update_snapshot(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.UpdateSnapshotRequest, dict]): The request object. Request for the UpdateSnapshot @@ -1529,6 +1856,23 @@ async def delete_snapshot( no association with the old snapshot or its subscription, unless the same subscription is specified. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + client.delete_snapshot(request=request) + Args: request (Union[google.pubsub_v1.types.DeleteSnapshotRequest, dict]): The request object. Request for the `DeleteSnapshot` @@ -1547,7 +1891,7 @@ async def delete_snapshot( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([snapshot]) if request is not None and has_flattened_params: @@ -1609,6 +1953,26 @@ async def seek( Note that both the subscription and the snapshot must be on the same topic. + + .. code-block:: python + + from google import pubsub_v1 + + def sample_seek(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.SeekRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.seek(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.SeekRequest, dict]): The request object. Request for the `Seek` method. diff --git a/google/pubsub_v1/services/subscriber/client.py b/google/pubsub_v1/services/subscriber/client.py index 308da6f74..01499a881 100644 --- a/google/pubsub_v1/services/subscriber/client.py +++ b/google/pubsub_v1/services/subscriber/client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -281,6 +281,74 @@ def parse_common_location_path(path: str) -> Dict[str, str]: m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) return m.groupdict() if m else {} + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_client_cert not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert == "true": + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + def __init__( self, *, @@ -332,57 +400,22 @@ def __init__( if client_options is None: client_options = client_options_lib.ClientOptions() - # Create SSL credentials for mutual TLS if needed. - if os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") not in ( - "true", - "false", - ): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true" + api_endpoint, client_cert_source_func = self.get_mtls_endpoint_and_cert_source( + client_options ) - client_cert_source_func = None - is_mtls = False - if use_client_cert: - if client_options.client_cert_source: - is_mtls = True - client_cert_source_func = client_options.client_cert_source - else: - is_mtls = mtls.has_default_client_cert_source() - if is_mtls: - client_cert_source_func = mtls.default_client_cert_source() - else: - client_cert_source_func = None - - # Figure out which api endpoint to use. - if client_options.api_endpoint is not None: - api_endpoint = client_options.api_endpoint - else: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_mtls_env == "never": - api_endpoint = self.DEFAULT_ENDPOINT - elif use_mtls_env == "always": - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - elif use_mtls_env == "auto": - if is_mtls: - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = self.DEFAULT_ENDPOINT - else: - raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " - "values: never, auto, always" - ) + api_key_value = getattr(client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) # Save or instantiate the transport. # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, SubscriberTransport): # transport is a SubscriberTransport instance. - if credentials or client_options.credentials_file: + if credentials or client_options.credentials_file or api_key_value: raise ValueError( "When providing a transport instance, " "provide its credentials directly." @@ -394,6 +427,15 @@ def __init__( ) self._transport = transport else: + import google.auth._default # type: ignore + + if api_key_value and hasattr( + google.auth._default, "get_api_key_credentials" + ): + credentials = google.auth._default.get_api_key_credentials( + api_key_value + ) + Transport = type(self).get_transport_class(transport) emulator_host = os.environ.get("PUBSUB_EMULATOR_HOST") @@ -442,6 +484,27 @@ def create_subscription( request. + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.Subscription( + name="name_value", + topic="topic_value", + ) + + # Make the request + response = client.create_subscription(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.Subscription, dict]): The request object. A subscription resource. @@ -518,7 +581,7 @@ def create_subscription( A subscription resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name, topic, push_config, ack_deadline_seconds]) if request is not None and has_flattened_params: @@ -571,6 +634,25 @@ def get_subscription( ) -> pubsub.Subscription: r"""Gets the configuration details of a subscription. + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.get_subscription(request=request) + + # Handle the response + print(response) + Args: request (Union[google.pubsub_v1.types.GetSubscriptionRequest, dict]): @@ -594,7 +676,7 @@ def get_subscription( A subscription resource. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription]) if request is not None and has_flattened_params: @@ -645,6 +727,30 @@ def update_subscription( modifiable. + .. code-block:: python + + from google import pubsub_v1 + + def sample_update_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + subscription = pubsub_v1.Subscription() + subscription.name = "name_value" + subscription.topic = "topic_value" + + request = pubsub_v1.UpdateSubscriptionRequest( + subscription=subscription, + ) + + # Make the request + response = client.update_subscription(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.UpdateSubscriptionRequest, dict]): The request object. Request for the UpdateSubscription @@ -696,6 +802,26 @@ def list_subscriptions( ) -> pagers.ListSubscriptionsPager: r"""Lists matching subscriptions. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_subscriptions(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSubscriptionsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_subscriptions(request=request) + + # Handle the response + for response in page_result: + print(response) + Args: request (Union[google.pubsub_v1.types.ListSubscriptionsRequest, dict]): @@ -723,7 +849,7 @@ def list_subscriptions( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([project]) if request is not None and has_flattened_params: @@ -782,6 +908,23 @@ def delete_subscription( topic unless the same topic is specified. + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + client.delete_subscription(request=request) + + Args: request (Union[google.pubsub_v1.types.DeleteSubscriptionRequest, dict]): The request object. Request for the DeleteSubscription @@ -800,7 +943,7 @@ def delete_subscription( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription]) if request is not None and has_flattened_params: @@ -856,6 +999,25 @@ def modify_ack_deadline( used for subsequent messages. + .. code-block:: python + + from google import pubsub_v1 + + def sample_modify_ack_deadline(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyAckDeadlineRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ack_deadline_seconds=2066, + ) + + # Make the request + client.modify_ack_deadline(request=request) + + Args: request (Union[google.pubsub_v1.types.ModifyAckDeadlineRequest, dict]): The request object. Request for the ModifyAckDeadline @@ -895,7 +1057,7 @@ def modify_ack_deadline( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, ack_ids, ack_deadline_seconds]) if request is not None and has_flattened_params: @@ -956,6 +1118,24 @@ def acknowledge( error. + .. code-block:: python + + from google import pubsub_v1 + + def sample_acknowledge(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.AcknowledgeRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ) + + # Make the request + client.acknowledge(request=request) + + Args: request (Union[google.pubsub_v1.types.AcknowledgeRequest, dict]): The request object. Request for the Acknowledge method. @@ -982,7 +1162,7 @@ def acknowledge( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, ack_ids]) if request is not None and has_flattened_params: @@ -1037,6 +1217,27 @@ def pull( pending for the given subscription. + .. code-block:: python + + from google import pubsub_v1 + + def sample_pull(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.PullRequest( + subscription="subscription_value", + max_messages=1277, + ) + + # Make the request + response = client.pull(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.PullRequest, dict]): The request object. Request for the `Pull` method. @@ -1083,7 +1284,7 @@ def pull( Response for the Pull method. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, return_immediately, max_messages]) if request is not None and has_flattened_params: @@ -1149,6 +1350,38 @@ def streaming_pull( configuring the underlying RPC channel. + .. code-block:: python + + from google import pubsub_v1 + + def sample_streaming_pull(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.StreamingPullRequest( + subscription="subscription_value", + stream_ack_deadline_seconds=2813, + ) + + # This method expects an iterator which contains + # 'pubsub_v1.StreamingPullRequest' objects + # Here we create a generator that yields a single `request` for + # demonstrative purposes. + requests = [request] + + def request_generator(): + for request in requests: + yield request + + # Make the request + stream = client.streaming_pull(requests=request_generator()) + + # Handle the response + for response in stream: + print(response) + + Args: requests (Iterator[google.pubsub_v1.types.StreamingPullRequest]): The request object iterator. Request for the `StreamingPull` @@ -1203,6 +1436,23 @@ def modify_push_config( call regardless of changes to the ``PushConfig``. + .. code-block:: python + + from google import pubsub_v1 + + def sample_modify_push_config(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyPushConfigRequest( + subscription="subscription_value", + ) + + # Make the request + client.modify_push_config(request=request) + + Args: request (Union[google.pubsub_v1.types.ModifyPushConfigRequest, dict]): The request object. Request for the ModifyPushConfig @@ -1233,7 +1483,7 @@ def modify_push_config( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([subscription, push_config]) if request is not None and has_flattened_params: @@ -1283,13 +1533,33 @@ def get_snapshot( ) -> pubsub.Snapshot: r"""Gets the configuration details of a snapshot. Snapshots are used in Seek operations, which allow you to manage - message acknowledgments in bulk. That is, you can set - the acknowledgment state of messages in an existing + href="https://cloud.google.com/pubsub/docs/replay-overview">Seek + operations, which allow you to manage message + acknowledgments in bulk. That is, you can set the + acknowledgment state of messages in an existing subscription to the state captured by a snapshot. + .. code-block:: python + + from google import pubsub_v1 + + def sample_get_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + response = client.get_snapshot(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.GetSnapshotRequest, dict]): The request object. Request for the GetSnapshot method. @@ -1317,7 +1587,7 @@ def get_snapshot( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([snapshot]) if request is not None and has_flattened_params: @@ -1369,6 +1639,27 @@ def list_snapshots( in an existing subscription to the state captured by a snapshot. + .. code-block:: python + + from google import pubsub_v1 + + def sample_list_snapshots(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSnapshotsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_snapshots(request=request) + + # Handle the response + for response in page_result: + print(response) + + Args: request (Union[google.pubsub_v1.types.ListSnapshotsRequest, dict]): The request object. Request for the `ListSnapshots` @@ -1395,7 +1686,7 @@ def list_snapshots( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([project]) if request is not None and has_flattened_params: @@ -1468,6 +1759,27 @@ def create_snapshot( request. + .. code-block:: python + + from google import pubsub_v1 + + def sample_create_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.CreateSnapshotRequest( + name="name_value", + subscription="subscription_value", + ) + + # Make the request + response = client.create_snapshot(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.CreateSnapshotRequest, dict]): The request object. Request for the `CreateSnapshot` @@ -1516,7 +1828,7 @@ def create_snapshot( """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([name, subscription]) if request is not None and has_flattened_params: @@ -1563,14 +1875,34 @@ def update_snapshot( metadata: Sequence[Tuple[str, str]] = (), ) -> pubsub.Snapshot: r"""Updates an existing snapshot. Snapshots are used in - Seek operations, which allow + Seek + operations, which allow you to manage message acknowledgments in bulk. That is, you can set the acknowledgment state of messages in an existing subscription to the state captured by a snapshot. + .. code-block:: python + + from google import pubsub_v1 + + def sample_update_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.UpdateSnapshotRequest( + ) + + # Make the request + response = client.update_snapshot(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.UpdateSnapshotRequest, dict]): The request object. Request for the UpdateSnapshot @@ -1638,6 +1970,23 @@ def delete_snapshot( the same subscription is specified. + .. code-block:: python + + from google import pubsub_v1 + + def sample_delete_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + client.delete_snapshot(request=request) + + Args: request (Union[google.pubsub_v1.types.DeleteSnapshotRequest, dict]): The request object. Request for the `DeleteSnapshot` @@ -1656,7 +2005,7 @@ def delete_snapshot( sent along with the request as metadata. """ # Create or coerce a protobuf request object. - # Sanity check: If we got a request object, we should *not* have + # Quick check: If we got a request object, we should *not* have # gotten any keyword arguments that map to the request. has_flattened_params = any([snapshot]) if request is not None and has_flattened_params: @@ -1710,6 +2059,26 @@ def seek( same topic. + .. code-block:: python + + from google import pubsub_v1 + + def sample_seek(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.SeekRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.seek(request=request) + + # Handle the response + print(response) + + Args: request (Union[google.pubsub_v1.types.SeekRequest, dict]): The request object. Request for the `Seek` method. diff --git a/google/pubsub_v1/services/subscriber/pagers.py b/google/pubsub_v1/services/subscriber/pagers.py index ffd17c840..cb3896bb5 100644 --- a/google/pubsub_v1/services/subscriber/pagers.py +++ b/google/pubsub_v1/services/subscriber/pagers.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/subscriber/transports/__init__.py b/google/pubsub_v1/services/subscriber/transports/__init__.py index 023406c8f..f71cdecd4 100644 --- a/google/pubsub_v1/services/subscriber/transports/__init__.py +++ b/google/pubsub_v1/services/subscriber/transports/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/services/subscriber/transports/base.py b/google/pubsub_v1/services/subscriber/transports/base.py index 6d90cef94..3cd6e7972 100644 --- a/google/pubsub_v1/services/subscriber/transports/base.py +++ b/google/pubsub_v1/services/subscriber/transports/base.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -107,7 +107,6 @@ def __init__( credentials, _ = google.auth.load_credentials_from_file( credentials_file, **scopes_kwargs, quota_project_id=quota_project_id ) - elif credentials is None: credentials, _ = google.auth.default( **scopes_kwargs, quota_project_id=quota_project_id diff --git a/google/pubsub_v1/services/subscriber/transports/grpc.py b/google/pubsub_v1/services/subscriber/transports/grpc.py index f0472bdd0..130dca2c1 100644 --- a/google/pubsub_v1/services/subscriber/transports/grpc.py +++ b/google/pubsub_v1/services/subscriber/transports/grpc.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -164,8 +164,11 @@ def __init__( if not self._grpc_channel: self._grpc_channel = type(self).create_channel( self._host, + # use the credentials which are saved credentials=self._credentials, - credentials_file=credentials_file, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, @@ -540,10 +543,10 @@ def get_snapshot(self) -> Callable[[pubsub.GetSnapshotRequest], pubsub.Snapshot] Gets the configuration details of a snapshot. Snapshots are used in Seek operations, which allow you to manage - message acknowledgments in bulk. That is, you can set - the acknowledgment state of messages in an existing + href="https://cloud.google.com/pubsub/docs/replay-overview">Seek + operations, which allow you to manage message + acknowledgments in bulk. That is, you can set the + acknowledgment state of messages in an existing subscription to the state captured by a snapshot. Returns: @@ -645,8 +648,9 @@ def update_snapshot( r"""Return a callable for the update snapshot method over gRPC. Updates an existing snapshot. Snapshots are used in - Seek operations, which allow + Seek + operations, which allow you to manage message acknowledgments in bulk. That is, you can set the acknowledgment state of messages in an existing subscription to the state captured by a diff --git a/google/pubsub_v1/services/subscriber/transports/grpc_asyncio.py b/google/pubsub_v1/services/subscriber/transports/grpc_asyncio.py index 9a4b45224..a6a19fb6a 100644 --- a/google/pubsub_v1/services/subscriber/transports/grpc_asyncio.py +++ b/google/pubsub_v1/services/subscriber/transports/grpc_asyncio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -209,8 +209,11 @@ def __init__( if not self._grpc_channel: self._grpc_channel = type(self).create_channel( self._host, + # use the credentials which are saved credentials=self._credentials, - credentials_file=credentials_file, + # Set ``credentials_file`` to ``None`` here as + # the credentials that we saved earlier should be used. + credentials_file=None, scopes=self._scopes, ssl_credentials=self._ssl_channel_credentials, quota_project_id=quota_project_id, @@ -550,10 +553,10 @@ def get_snapshot( Gets the configuration details of a snapshot. Snapshots are used in Seek operations, which allow you to manage - message acknowledgments in bulk. That is, you can set - the acknowledgment state of messages in an existing + href="https://cloud.google.com/pubsub/docs/replay-overview">Seek + operations, which allow you to manage message + acknowledgments in bulk. That is, you can set the + acknowledgment state of messages in an existing subscription to the state captured by a snapshot. Returns: @@ -657,8 +660,9 @@ def update_snapshot( r"""Return a callable for the update snapshot method over gRPC. Updates an existing snapshot. Snapshots are used in - Seek operations, which allow + Seek + operations, which allow you to manage message acknowledgments in bulk. That is, you can set the acknowledgment state of messages in an existing subscription to the state captured by a diff --git a/google/pubsub_v1/types/__init__.py b/google/pubsub_v1/types/__init__.py index ebc8b5399..888e2184a 100644 --- a/google/pubsub_v1/types/__init__.py +++ b/google/pubsub_v1/types/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/pubsub_v1/types/pubsub.py b/google/pubsub_v1/types/pubsub.py index de29dcd10..3b79e8eca 100644 --- a/google/pubsub_v1/types/pubsub.py +++ b/google/pubsub_v1/types/pubsub.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -558,6 +558,22 @@ class Subscription(proto.Message): ``StreamingPull`` requests will return FAILED_PRECONDITION. If the subscription is a push subscription, pushes to the endpoint will not be made. + enable_exactly_once_delivery (bool): + If true, Pub/Sub provides the following guarantees for the + delivery of a message with a given value of ``message_id`` + on this subscription: + + - The message sent to a subscriber is guaranteed not to be + resent before the message's acknowledgement deadline + expires. + - An acknowledged message will not be resent to a + subscriber. + + Note that subscribers may still receive multiple copies of a + message when ``enable_exactly_once_delivery`` is true if the + message was published multiple times by a publisher client. + These copies are considered distinct by Pub/Sub and have + distinct ``message_id`` values. topic_message_retention_duration (google.protobuf.duration_pb2.Duration): Output only. Indicates the minimum duration for which a message is retained after it is published to the @@ -588,6 +604,7 @@ class Subscription(proto.Message): ) retry_policy = proto.Field(proto.MESSAGE, number=14, message="RetryPolicy",) detached = proto.Field(proto.BOOL, number=15,) + enable_exactly_once_delivery = proto.Field(proto.BOOL, number=16,) topic_message_retention_duration = proto.Field( proto.MESSAGE, number=17, message=duration_pb2.Duration, ) @@ -1106,24 +1123,76 @@ class StreamingPullResponse(proto.Message): received_messages (Sequence[google.pubsub_v1.types.ReceivedMessage]): Received Pub/Sub messages. This will not be empty. + acknowledge_confirmation (google.pubsub_v1.types.StreamingPullResponse.AcknowledgeConfirmation): + This field will only be set if + ``enable_exactly_once_delivery`` is set to ``true``. + modify_ack_deadline_confirmation (google.pubsub_v1.types.StreamingPullResponse.ModifyAckDeadlineConfirmation): + This field will only be set if + ``enable_exactly_once_delivery`` is set to ``true``. subscription_properties (google.pubsub_v1.types.StreamingPullResponse.SubscriptionProperties): Properties associated with this subscription. """ + class AcknowledgeConfirmation(proto.Message): + r"""Acknowledgement IDs sent in one or more previous requests to + acknowledge a previously received message. + + Attributes: + ack_ids (Sequence[str]): + Successfully processed acknowledgement IDs. + invalid_ack_ids (Sequence[str]): + List of acknowledgement IDs that were + malformed or whose acknowledgement deadline has + expired. + unordered_ack_ids (Sequence[str]): + List of acknowledgement IDs that were out of + order. + """ + + ack_ids = proto.RepeatedField(proto.STRING, number=1,) + invalid_ack_ids = proto.RepeatedField(proto.STRING, number=2,) + unordered_ack_ids = proto.RepeatedField(proto.STRING, number=3,) + + class ModifyAckDeadlineConfirmation(proto.Message): + r"""Acknowledgement IDs sent in one or more previous requests to + modify the deadline for a specific message. + + Attributes: + ack_ids (Sequence[str]): + Successfully processed acknowledgement IDs. + invalid_ack_ids (Sequence[str]): + List of acknowledgement IDs that were + malformed or whose acknowledgement deadline has + expired. + """ + + ack_ids = proto.RepeatedField(proto.STRING, number=1,) + invalid_ack_ids = proto.RepeatedField(proto.STRING, number=2,) + class SubscriptionProperties(proto.Message): r"""Subscription properties sent as part of the response. Attributes: + exactly_once_delivery_enabled (bool): + True iff exactly once delivery is enabled for + this subscription. message_ordering_enabled (bool): True iff message ordering is enabled for this subscription. """ + exactly_once_delivery_enabled = proto.Field(proto.BOOL, number=1,) message_ordering_enabled = proto.Field(proto.BOOL, number=2,) received_messages = proto.RepeatedField( proto.MESSAGE, number=1, message="ReceivedMessage", ) + acknowledge_confirmation = proto.Field( + proto.MESSAGE, number=5, message=AcknowledgeConfirmation, + ) + modify_ack_deadline_confirmation = proto.Field( + proto.MESSAGE, number=3, message=ModifyAckDeadlineConfirmation, + ) subscription_properties = proto.Field( proto.MESSAGE, number=4, message=SubscriptionProperties, ) diff --git a/google/pubsub_v1/types/schema.py b/google/pubsub_v1/types/schema.py index 3e389af46..b0a4d4e0a 100644 --- a/google/pubsub_v1/types/schema.py +++ b/google/pubsub_v1/types/schema.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/noxfile.py b/noxfile.py index 931c23ad2..e9fea8af8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -27,8 +27,7 @@ BLACK_VERSION = "black==19.10b0" BLACK_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] -PYTYPE_VERSION = "pytype==2021.4.9" - +MYPY_VERSION = "mypy==0.910" DEFAULT_PYTHON_VERSION = "3.8" SYSTEM_TEST_PYTHON_VERSIONS = ["3.10"] @@ -44,7 +43,9 @@ "lint", "lint_setup_py", "blacken", - "pytype", + "mypy", + # https://github.com/googleapis/python-pubsub/pull/552#issuecomment-1016256936 + # "mypy_samples", # TODO: uncomment when the check passes "docs", ] @@ -53,11 +54,47 @@ @nox.session(python=DEFAULT_PYTHON_VERSION) -def pytype(session): - """Run type checks.""" +def mypy(session): + """Run type checks with mypy.""" + session.install("-e", ".[all]") + session.install(MYPY_VERSION) + + # Just install the type info directly, since "mypy --install-types" might + # require an additional pass. + session.install("types-protobuf", "types-setuptools") + + # Version 2.1.1 of google-api-core version is the first type-checked release. + # Version 2.2.0 of google-cloud-core version is the first type-checked release. + session.install( + "google-api-core[grpc]>=2.1.1", "google-cloud-core>=2.2.0", + ) + + # TODO: Only check the hand-written layer, the generated code does not pass + # mypy checks yet. + # https://github.com/googleapis/gapic-generator-python/issues/1092 + session.run("mypy", "google/cloud") + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def mypy_samples(session): + """Run type checks with mypy.""" + session.install("-e", ".[all]") - session.install(PYTYPE_VERSION) - session.run("pytype") + + session.install("pytest") + session.install(MYPY_VERSION) + + # Just install the type info directly, since "mypy --install-types" might + # require an additional pass. + session.install("types-mock", "types-protobuf", "types-setuptools") + + session.run( + "mypy", + "--config-file", + str(CURRENT_DIRECTORY / "samples" / "snippets" / "mypy.ini"), + "--no-incremental", # Required by warn-unused-configs from mypy.ini to work + "samples/", + ) @nox.session(python=DEFAULT_PYTHON_VERSION) diff --git a/owlbot.py b/owlbot.py index 06031ff37..570d61ca4 100644 --- a/owlbot.py +++ b/owlbot.py @@ -232,7 +232,7 @@ library / f"google/pubsub_{library.name}/services/subscriber/transports/base.py", ], - r"""gapic_version=(pkg_resources\.get_distribution\(\s+)['"]google-pubsub['"]""", + r"""gapic_version=(pkg_resources\.get_distribution\(\s+)['"]google-cloud-pubsub['"]""", "client_library_version=\g<1>'google-cloud-pubsub'", ) @@ -332,6 +332,14 @@ if count < 1: raise Exception(".coveragerc replacement failed.") + # fix the package name in samples/generated_samples to reflect + # the package on pypi. https://pypi.org/project/google-cloud-pubsub/ + s.replace( + library / "samples/generated_samples/**/*.py", + "pip install google-pubsub", + "pip install google-cloud-pubsub", + ) + s.move( library, excludes=[ @@ -365,61 +373,92 @@ python.py_samples() # ---------------------------------------------------------------------------- -# pytype-related changes +# Add mypy nox session. # ---------------------------------------------------------------------------- - -# Add .pytype to .gitignore -s.replace(".gitignore", r"\.pytest_cache", "\g<0>\n.pytype") - -# Add pytype config to setup.cfg s.replace( - "setup.cfg", - r"universal = 1", + "noxfile.py", + r"BLACK_PATHS = \[.*?\]", + '\g<0>\n\nMYPY_VERSION = "mypy==0.910"', +) +s.replace( + "noxfile.py", r'"blacken",', '\g<0>\n "mypy",', +) +s.replace( + "noxfile.py", + r"nox\.options\.error_on_missing_interpreters = True", textwrap.dedent( - """ \g<0> - - [pytype] - python_version = 3.8 - inputs = - google/cloud/ - exclude = - tests/ - google/pubsub_v1/ # generated code - output = .pytype/ - disable = - # There's some issue with finding some pyi files, thus disabling. - # The issue https://github.com/google/pytype/issues/150 is closed, but the - # error still occurs for some reason. - pyi-error""" + ''' \g<0> + + + @nox.session(python=DEFAULT_PYTHON_VERSION) + def mypy(session): + """Run type checks with mypy.""" + session.install("-e", ".[all]") + session.install(MYPY_VERSION) + + # Just install the type info directly, since "mypy --install-types" might + # require an additional pass. + session.install("types-protobuf", "types-setuptools") + + # Version 2.1.1 of google-api-core version is the first type-checked release. + # Version 2.2.0 of google-cloud-core version is the first type-checked release. + session.install( + "google-api-core[grpc]>=2.1.1", + "google-cloud-core>=2.2.0", + ) + + # TODO: Only check the hand-written layer, the generated code does not pass + # mypy checks yet. + # https://github.com/googleapis/gapic-generator-python/issues/1092 + session.run("mypy", "google/cloud")''' ), ) -# Add pytype session to noxfile.py + +# ---------------------------------------------------------------------------- +# Add mypy_samples nox session. +# ---------------------------------------------------------------------------- s.replace( "noxfile.py", - r"BLACK_PATHS = \[.*?\]", - '\g<0>\n\nPYTYPE_VERSION = "pytype==2021.4.9"\n', -) -s.replace( - "noxfile.py", r'"blacken",', '\g<0>\n "pytype",', + r' "mypy",', + '\g<0>\n # https://github.com/googleapis/python-pubsub/pull/552#issuecomment-1016256936' + '\n # "mypy_samples", # TODO: uncomment when the check passes', ) s.replace( "noxfile.py", - r"nox\.options\.error_on_missing_interpreters = True", + r'session\.run\("mypy", "google/cloud"\)', textwrap.dedent( ''' \g<0> @nox.session(python=DEFAULT_PYTHON_VERSION) - def pytype(session): - """Run type checks.""" + def mypy_samples(session): + """Run type checks with mypy.""" + session.install("-e", ".[all]") - session.install(PYTYPE_VERSION) - session.run("pytype")''' + + session.install("pytest") + session.install(MYPY_VERSION) + + # Just install the type info directly, since "mypy --install-types" might + # require an additional pass. + session.install("types-mock", "types-protobuf", "types-setuptools") + + session.run( + "mypy", + "--config-file", + str(CURRENT_DIRECTORY / "samples" / "snippets" / "mypy.ini"), + "--no-incremental", # Required by warn-unused-configs from mypy.ini to work + "samples/", + )''' ), ) + + +# Only consider the hand-written layer when assessing the test coverage. s.replace( "noxfile.py", "--cov=google", "--cov=google/cloud", ) +# Final code style adjustments. s.shell.run(["nox", "-s", "blacken"], hide_output=False) diff --git a/samples/AUTHORING_GUIDE.md b/samples/AUTHORING_GUIDE.md index 55c97b32f..8249522ff 100644 --- a/samples/AUTHORING_GUIDE.md +++ b/samples/AUTHORING_GUIDE.md @@ -1 +1 @@ -See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/AUTHORING_GUIDE.md \ No newline at end of file +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md \ No newline at end of file diff --git a/samples/CONTRIBUTING.md b/samples/CONTRIBUTING.md index 34c882b6f..f5fe2e6ba 100644 --- a/samples/CONTRIBUTING.md +++ b/samples/CONTRIBUTING.md @@ -1 +1 @@ -See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/CONTRIBUTING.md \ No newline at end of file +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/CONTRIBUTING.md \ No newline at end of file diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_create_topic_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_create_topic_async.py new file mode 100644 index 000000000..e79f28c98 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_create_topic_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_CreateTopic_async] +from google import pubsub_v1 + + +async def sample_create_topic(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.Topic( + name="name_value", + ) + + # Make the request + response = await client.create_topic(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_CreateTopic_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_create_topic_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_create_topic_sync.py new file mode 100644 index 000000000..6a6f04a27 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_create_topic_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_CreateTopic_sync] +from google import pubsub_v1 + + +def sample_create_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.Topic( + name="name_value", + ) + + # Make the request + response = client.create_topic(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_CreateTopic_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_delete_topic_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_delete_topic_async.py new file mode 100644 index 000000000..2a0148abb --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_delete_topic_async.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_DeleteTopic_async] +from google import pubsub_v1 + + +async def sample_delete_topic(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteTopicRequest( + topic="topic_value", + ) + + # Make the request + await client.delete_topic(request=request) + + +# [END pubsub_v1_generated_Publisher_DeleteTopic_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_delete_topic_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_delete_topic_sync.py new file mode 100644 index 000000000..376a93ba0 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_delete_topic_sync.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_DeleteTopic_sync] +from google import pubsub_v1 + + +def sample_delete_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteTopicRequest( + topic="topic_value", + ) + + # Make the request + client.delete_topic(request=request) + + +# [END pubsub_v1_generated_Publisher_DeleteTopic_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_detach_subscription_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_detach_subscription_async.py new file mode 100644 index 000000000..6fb8d4e7d --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_detach_subscription_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DetachSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_DetachSubscription_async] +from google import pubsub_v1 + + +async def sample_detach_subscription(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.DetachSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = await client.detach_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_DetachSubscription_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_detach_subscription_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_detach_subscription_sync.py new file mode 100644 index 000000000..7c36e4df1 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_detach_subscription_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DetachSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_DetachSubscription_sync] +from google import pubsub_v1 + + +def sample_detach_subscription(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.DetachSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.detach_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_DetachSubscription_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_get_topic_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_get_topic_async.py new file mode 100644 index 000000000..87904db2b --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_get_topic_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_GetTopic_async] +from google import pubsub_v1 + + +async def sample_get_topic(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.GetTopicRequest( + topic="topic_value", + ) + + # Make the request + response = await client.get_topic(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_GetTopic_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_get_topic_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_get_topic_sync.py new file mode 100644 index 000000000..2f28cef0a --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_get_topic_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_GetTopic_sync] +from google import pubsub_v1 + + +def sample_get_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.GetTopicRequest( + topic="topic_value", + ) + + # Make the request + response = client.get_topic(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_GetTopic_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_snapshots_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_snapshots_async.py new file mode 100644 index 000000000..b6388f7f5 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_snapshots_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListTopicSnapshots +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_ListTopicSnapshots_async] +from google import pubsub_v1 + + +async def sample_list_topic_snapshots(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSnapshotsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_snapshots(request=request) + + # Handle the response + async for response in page_result: + print(response) + +# [END pubsub_v1_generated_Publisher_ListTopicSnapshots_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_snapshots_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_snapshots_sync.py new file mode 100644 index 000000000..f7f3a61ec --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_snapshots_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListTopicSnapshots +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_ListTopicSnapshots_sync] +from google import pubsub_v1 + + +def sample_list_topic_snapshots(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSnapshotsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_snapshots(request=request) + + # Handle the response + for response in page_result: + print(response) + +# [END pubsub_v1_generated_Publisher_ListTopicSnapshots_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_subscriptions_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_subscriptions_async.py new file mode 100644 index 000000000..59b35194b --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_subscriptions_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListTopicSubscriptions +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_ListTopicSubscriptions_async] +from google import pubsub_v1 + + +async def sample_list_topic_subscriptions(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSubscriptionsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_subscriptions(request=request) + + # Handle the response + async for response in page_result: + print(response) + +# [END pubsub_v1_generated_Publisher_ListTopicSubscriptions_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_subscriptions_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_subscriptions_sync.py new file mode 100644 index 000000000..d7dffa0e2 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_list_topic_subscriptions_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListTopicSubscriptions +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_ListTopicSubscriptions_sync] +from google import pubsub_v1 + + +def sample_list_topic_subscriptions(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicSubscriptionsRequest( + topic="topic_value", + ) + + # Make the request + page_result = client.list_topic_subscriptions(request=request) + + # Handle the response + for response in page_result: + print(response) + +# [END pubsub_v1_generated_Publisher_ListTopicSubscriptions_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_list_topics_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_list_topics_async.py new file mode 100644 index 000000000..0d0f10a98 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_list_topics_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListTopics +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_ListTopics_async] +from google import pubsub_v1 + + +async def sample_list_topics(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_topics(request=request) + + # Handle the response + async for response in page_result: + print(response) + +# [END pubsub_v1_generated_Publisher_ListTopics_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_list_topics_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_list_topics_sync.py new file mode 100644 index 000000000..cffdd77a4 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_list_topics_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListTopics +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_ListTopics_sync] +from google import pubsub_v1 + + +def sample_list_topics(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.ListTopicsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_topics(request=request) + + # Handle the response + for response in page_result: + print(response) + +# [END pubsub_v1_generated_Publisher_ListTopics_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_publish_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_publish_async.py new file mode 100644 index 000000000..98bfc618e --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_publish_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Publish +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_Publish_async] +from google import pubsub_v1 + + +async def sample_publish(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.PublishRequest( + topic="topic_value", + ) + + # Make the request + response = await client.publish(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_Publish_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_publish_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_publish_sync.py new file mode 100644 index 000000000..650440a78 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_publish_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Publish +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_Publish_sync] +from google import pubsub_v1 + + +def sample_publish(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + request = pubsub_v1.PublishRequest( + topic="topic_value", + ) + + # Make the request + response = client.publish(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_Publish_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_update_topic_async.py b/samples/generated_samples/pubsub_v1_generated_publisher_update_topic_async.py new file mode 100644 index 000000000..473144d07 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_update_topic_async.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for UpdateTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_UpdateTopic_async] +from google import pubsub_v1 + + +async def sample_update_topic(): + # Create a client + client = pubsub_v1.PublisherAsyncClient() + + # Initialize request argument(s) + topic = pubsub_v1.Topic() + topic.name = "name_value" + + request = pubsub_v1.UpdateTopicRequest( + topic=topic, + ) + + # Make the request + response = await client.update_topic(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_UpdateTopic_async] diff --git a/samples/generated_samples/pubsub_v1_generated_publisher_update_topic_sync.py b/samples/generated_samples/pubsub_v1_generated_publisher_update_topic_sync.py new file mode 100644 index 000000000..5a9838c2a --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_publisher_update_topic_sync.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for UpdateTopic +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Publisher_UpdateTopic_sync] +from google import pubsub_v1 + + +def sample_update_topic(): + # Create a client + client = pubsub_v1.PublisherClient() + + # Initialize request argument(s) + topic = pubsub_v1.Topic() + topic.name = "name_value" + + request = pubsub_v1.UpdateTopicRequest( + topic=topic, + ) + + # Make the request + response = client.update_topic(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Publisher_UpdateTopic_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_create_schema_async.py b/samples/generated_samples/pubsub_v1_generated_schema_service_create_schema_async.py new file mode 100644 index 000000000..9f9790725 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_create_schema_async.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_CreateSchema_async] +from google import pubsub_v1 + + +async def sample_create_schema(): + # Create a client + client = pubsub_v1.SchemaServiceAsyncClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.CreateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = await client.create_schema(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_CreateSchema_async] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_create_schema_sync.py b/samples/generated_samples/pubsub_v1_generated_schema_service_create_schema_sync.py new file mode 100644 index 000000000..798194050 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_create_schema_sync.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_CreateSchema_sync] +from google import pubsub_v1 + + +def sample_create_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.CreateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = client.create_schema(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_CreateSchema_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_delete_schema_async.py b/samples/generated_samples/pubsub_v1_generated_schema_service_delete_schema_async.py new file mode 100644 index 000000000..6d5e8f734 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_delete_schema_async.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_DeleteSchema_async] +from google import pubsub_v1 + + +async def sample_delete_schema(): + # Create a client + client = pubsub_v1.SchemaServiceAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSchemaRequest( + name="name_value", + ) + + # Make the request + await client.delete_schema(request=request) + + +# [END pubsub_v1_generated_SchemaService_DeleteSchema_async] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_delete_schema_sync.py b/samples/generated_samples/pubsub_v1_generated_schema_service_delete_schema_sync.py new file mode 100644 index 000000000..2e516b97a --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_delete_schema_sync.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_DeleteSchema_sync] +from google import pubsub_v1 + + +def sample_delete_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSchemaRequest( + name="name_value", + ) + + # Make the request + client.delete_schema(request=request) + + +# [END pubsub_v1_generated_SchemaService_DeleteSchema_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_get_schema_async.py b/samples/generated_samples/pubsub_v1_generated_schema_service_get_schema_async.py new file mode 100644 index 000000000..10db352c3 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_get_schema_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_GetSchema_async] +from google import pubsub_v1 + + +async def sample_get_schema(): + # Create a client + client = pubsub_v1.SchemaServiceAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSchemaRequest( + name="name_value", + ) + + # Make the request + response = await client.get_schema(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_GetSchema_async] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_get_schema_sync.py b/samples/generated_samples/pubsub_v1_generated_schema_service_get_schema_sync.py new file mode 100644 index 000000000..7d3cdf6d1 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_get_schema_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_GetSchema_sync] +from google import pubsub_v1 + + +def sample_get_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSchemaRequest( + name="name_value", + ) + + # Make the request + response = client.get_schema(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_GetSchema_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_list_schemas_async.py b/samples/generated_samples/pubsub_v1_generated_schema_service_list_schemas_async.py new file mode 100644 index 000000000..a1c9be6ee --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_list_schemas_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListSchemas +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_ListSchemas_async] +from google import pubsub_v1 + + +async def sample_list_schemas(): + # Create a client + client = pubsub_v1.SchemaServiceAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSchemasRequest( + parent="parent_value", + ) + + # Make the request + page_result = client.list_schemas(request=request) + + # Handle the response + async for response in page_result: + print(response) + +# [END pubsub_v1_generated_SchemaService_ListSchemas_async] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_list_schemas_sync.py b/samples/generated_samples/pubsub_v1_generated_schema_service_list_schemas_sync.py new file mode 100644 index 000000000..4604da242 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_list_schemas_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListSchemas +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_ListSchemas_sync] +from google import pubsub_v1 + + +def sample_list_schemas(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSchemasRequest( + parent="parent_value", + ) + + # Make the request + page_result = client.list_schemas(request=request) + + # Handle the response + for response in page_result: + print(response) + +# [END pubsub_v1_generated_SchemaService_ListSchemas_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_validate_message_async.py b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_message_async.py new file mode 100644 index 000000000..94a699e53 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_message_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ValidateMessage +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_ValidateMessage_async] +from google import pubsub_v1 + + +async def sample_validate_message(): + # Create a client + client = pubsub_v1.SchemaServiceAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ValidateMessageRequest( + name="name_value", + parent="parent_value", + ) + + # Make the request + response = await client.validate_message(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_ValidateMessage_async] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_validate_message_sync.py b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_message_sync.py new file mode 100644 index 000000000..26e32efa1 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_message_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ValidateMessage +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_ValidateMessage_sync] +from google import pubsub_v1 + + +def sample_validate_message(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + request = pubsub_v1.ValidateMessageRequest( + name="name_value", + parent="parent_value", + ) + + # Make the request + response = client.validate_message(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_ValidateMessage_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_validate_schema_async.py b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_schema_async.py new file mode 100644 index 000000000..86647c7bd --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_schema_async.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ValidateSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_ValidateSchema_async] +from google import pubsub_v1 + + +async def sample_validate_schema(): + # Create a client + client = pubsub_v1.SchemaServiceAsyncClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.ValidateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = await client.validate_schema(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_ValidateSchema_async] diff --git a/samples/generated_samples/pubsub_v1_generated_schema_service_validate_schema_sync.py b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_schema_sync.py new file mode 100644 index 000000000..102fb75ed --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_schema_service_validate_schema_sync.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ValidateSchema +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_SchemaService_ValidateSchema_sync] +from google import pubsub_v1 + + +def sample_validate_schema(): + # Create a client + client = pubsub_v1.SchemaServiceClient() + + # Initialize request argument(s) + schema = pubsub_v1.Schema() + schema.name = "name_value" + + request = pubsub_v1.ValidateSchemaRequest( + parent="parent_value", + schema=schema, + ) + + # Make the request + response = client.validate_schema(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_SchemaService_ValidateSchema_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_acknowledge_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_acknowledge_async.py new file mode 100644 index 000000000..8f87241a1 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_acknowledge_async.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Acknowledge +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_Acknowledge_async] +from google import pubsub_v1 + + +async def sample_acknowledge(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.AcknowledgeRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ) + + # Make the request + await client.acknowledge(request=request) + + +# [END pubsub_v1_generated_Subscriber_Acknowledge_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_acknowledge_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_acknowledge_sync.py new file mode 100644 index 000000000..a56c55a33 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_acknowledge_sync.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Acknowledge +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_Acknowledge_sync] +from google import pubsub_v1 + + +def sample_acknowledge(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.AcknowledgeRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ) + + # Make the request + client.acknowledge(request=request) + + +# [END pubsub_v1_generated_Subscriber_Acknowledge_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_create_snapshot_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_create_snapshot_async.py new file mode 100644 index 000000000..6e2d45387 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_create_snapshot_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_CreateSnapshot_async] +from google import pubsub_v1 + + +async def sample_create_snapshot(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.CreateSnapshotRequest( + name="name_value", + subscription="subscription_value", + ) + + # Make the request + response = await client.create_snapshot(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_CreateSnapshot_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_create_snapshot_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_create_snapshot_sync.py new file mode 100644 index 000000000..b6145acb9 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_create_snapshot_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_CreateSnapshot_sync] +from google import pubsub_v1 + + +def sample_create_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.CreateSnapshotRequest( + name="name_value", + subscription="subscription_value", + ) + + # Make the request + response = client.create_snapshot(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_CreateSnapshot_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_create_subscription_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_create_subscription_async.py new file mode 100644 index 000000000..4c63c47cd --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_create_subscription_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_CreateSubscription_async] +from google import pubsub_v1 + + +async def sample_create_subscription(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.Subscription( + name="name_value", + topic="topic_value", + ) + + # Make the request + response = await client.create_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_CreateSubscription_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_create_subscription_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_create_subscription_sync.py new file mode 100644 index 000000000..6e37969f1 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_create_subscription_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for CreateSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_CreateSubscription_sync] +from google import pubsub_v1 + + +def sample_create_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.Subscription( + name="name_value", + topic="topic_value", + ) + + # Make the request + response = client.create_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_CreateSubscription_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_delete_snapshot_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_snapshot_async.py new file mode 100644 index 000000000..26e2c7aa7 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_snapshot_async.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_DeleteSnapshot_async] +from google import pubsub_v1 + + +async def sample_delete_snapshot(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + await client.delete_snapshot(request=request) + + +# [END pubsub_v1_generated_Subscriber_DeleteSnapshot_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_delete_snapshot_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_snapshot_sync.py new file mode 100644 index 000000000..f2538ddb0 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_snapshot_sync.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_DeleteSnapshot_sync] +from google import pubsub_v1 + + +def sample_delete_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + client.delete_snapshot(request=request) + + +# [END pubsub_v1_generated_Subscriber_DeleteSnapshot_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_delete_subscription_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_subscription_async.py new file mode 100644 index 000000000..f310d24b2 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_subscription_async.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_DeleteSubscription_async] +from google import pubsub_v1 + + +async def sample_delete_subscription(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + await client.delete_subscription(request=request) + + +# [END pubsub_v1_generated_Subscriber_DeleteSubscription_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_delete_subscription_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_subscription_sync.py new file mode 100644 index 000000000..c601dd663 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_delete_subscription_sync.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for DeleteSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_DeleteSubscription_sync] +from google import pubsub_v1 + + +def sample_delete_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.DeleteSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + client.delete_subscription(request=request) + + +# [END pubsub_v1_generated_Subscriber_DeleteSubscription_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_get_snapshot_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_get_snapshot_async.py new file mode 100644 index 000000000..3a56e4fbb --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_get_snapshot_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_GetSnapshot_async] +from google import pubsub_v1 + + +async def sample_get_snapshot(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + response = await client.get_snapshot(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_GetSnapshot_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_get_snapshot_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_get_snapshot_sync.py new file mode 100644 index 000000000..3a6cd24ca --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_get_snapshot_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_GetSnapshot_sync] +from google import pubsub_v1 + + +def sample_get_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSnapshotRequest( + snapshot="snapshot_value", + ) + + # Make the request + response = client.get_snapshot(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_GetSnapshot_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_get_subscription_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_get_subscription_async.py new file mode 100644 index 000000000..7ad718326 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_get_subscription_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_GetSubscription_async] +from google import pubsub_v1 + + +async def sample_get_subscription(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = await client.get_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_GetSubscription_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_get_subscription_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_get_subscription_sync.py new file mode 100644 index 000000000..d883e085d --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_get_subscription_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for GetSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_GetSubscription_sync] +from google import pubsub_v1 + + +def sample_get_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.GetSubscriptionRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.get_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_GetSubscription_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_list_snapshots_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_list_snapshots_async.py new file mode 100644 index 000000000..edc7976a1 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_list_snapshots_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListSnapshots +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ListSnapshots_async] +from google import pubsub_v1 + + +async def sample_list_snapshots(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSnapshotsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_snapshots(request=request) + + # Handle the response + async for response in page_result: + print(response) + +# [END pubsub_v1_generated_Subscriber_ListSnapshots_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_list_snapshots_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_list_snapshots_sync.py new file mode 100644 index 000000000..e67ca2a39 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_list_snapshots_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListSnapshots +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ListSnapshots_sync] +from google import pubsub_v1 + + +def sample_list_snapshots(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSnapshotsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_snapshots(request=request) + + # Handle the response + for response in page_result: + print(response) + +# [END pubsub_v1_generated_Subscriber_ListSnapshots_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_list_subscriptions_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_list_subscriptions_async.py new file mode 100644 index 000000000..01c45577a --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_list_subscriptions_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListSubscriptions +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ListSubscriptions_async] +from google import pubsub_v1 + + +async def sample_list_subscriptions(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSubscriptionsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_subscriptions(request=request) + + # Handle the response + async for response in page_result: + print(response) + +# [END pubsub_v1_generated_Subscriber_ListSubscriptions_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_list_subscriptions_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_list_subscriptions_sync.py new file mode 100644 index 000000000..272b0408d --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_list_subscriptions_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ListSubscriptions +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ListSubscriptions_sync] +from google import pubsub_v1 + + +def sample_list_subscriptions(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ListSubscriptionsRequest( + project="project_value", + ) + + # Make the request + page_result = client.list_subscriptions(request=request) + + # Handle the response + for response in page_result: + print(response) + +# [END pubsub_v1_generated_Subscriber_ListSubscriptions_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_modify_ack_deadline_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_ack_deadline_async.py new file mode 100644 index 000000000..b85c2033f --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_ack_deadline_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ModifyAckDeadline +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ModifyAckDeadline_async] +from google import pubsub_v1 + + +async def sample_modify_ack_deadline(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyAckDeadlineRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ack_deadline_seconds=2066, + ) + + # Make the request + await client.modify_ack_deadline(request=request) + + +# [END pubsub_v1_generated_Subscriber_ModifyAckDeadline_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_modify_ack_deadline_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_ack_deadline_sync.py new file mode 100644 index 000000000..ac0805db4 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_ack_deadline_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ModifyAckDeadline +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ModifyAckDeadline_sync] +from google import pubsub_v1 + + +def sample_modify_ack_deadline(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyAckDeadlineRequest( + subscription="subscription_value", + ack_ids=['ack_ids_value_1', 'ack_ids_value_2'], + ack_deadline_seconds=2066, + ) + + # Make the request + client.modify_ack_deadline(request=request) + + +# [END pubsub_v1_generated_Subscriber_ModifyAckDeadline_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_modify_push_config_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_push_config_async.py new file mode 100644 index 000000000..662823a1d --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_push_config_async.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ModifyPushConfig +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ModifyPushConfig_async] +from google import pubsub_v1 + + +async def sample_modify_push_config(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyPushConfigRequest( + subscription="subscription_value", + ) + + # Make the request + await client.modify_push_config(request=request) + + +# [END pubsub_v1_generated_Subscriber_ModifyPushConfig_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_modify_push_config_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_push_config_sync.py new file mode 100644 index 000000000..a7499941c --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_modify_push_config_sync.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for ModifyPushConfig +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_ModifyPushConfig_sync] +from google import pubsub_v1 + + +def sample_modify_push_config(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.ModifyPushConfigRequest( + subscription="subscription_value", + ) + + # Make the request + client.modify_push_config(request=request) + + +# [END pubsub_v1_generated_Subscriber_ModifyPushConfig_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_pull_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_pull_async.py new file mode 100644 index 000000000..113f3ddfc --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_pull_async.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Pull +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_Pull_async] +from google import pubsub_v1 + + +async def sample_pull(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.PullRequest( + subscription="subscription_value", + max_messages=1277, + ) + + # Make the request + response = await client.pull(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_Pull_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_pull_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_pull_sync.py new file mode 100644 index 000000000..abb47bfa1 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_pull_sync.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Pull +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_Pull_sync] +from google import pubsub_v1 + + +def sample_pull(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.PullRequest( + subscription="subscription_value", + max_messages=1277, + ) + + # Make the request + response = client.pull(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_Pull_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_seek_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_seek_async.py new file mode 100644 index 000000000..062c69409 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_seek_async.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Seek +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_Seek_async] +from google import pubsub_v1 + + +async def sample_seek(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.SeekRequest( + subscription="subscription_value", + ) + + # Make the request + response = await client.seek(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_Seek_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_seek_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_seek_sync.py new file mode 100644 index 000000000..f28570e7c --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_seek_sync.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for Seek +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_Seek_sync] +from google import pubsub_v1 + + +def sample_seek(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.SeekRequest( + subscription="subscription_value", + ) + + # Make the request + response = client.seek(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_Seek_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_streaming_pull_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_streaming_pull_async.py new file mode 100644 index 000000000..64c1e3748 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_streaming_pull_async.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for StreamingPull +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_StreamingPull_async] +from google import pubsub_v1 + + +async def sample_streaming_pull(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.StreamingPullRequest( + subscription="subscription_value", + stream_ack_deadline_seconds=2813, + ) + + # This method expects an iterator which contains + # 'pubsub_v1.StreamingPullRequest' objects + # Here we create a generator that yields a single `request` for + # demonstrative purposes. + requests = [request] + + def request_generator(): + for request in requests: + yield request + + # Make the request + stream = await client.streaming_pull(requests=request_generator()) + + # Handle the response + async for response in stream: + print(response) + +# [END pubsub_v1_generated_Subscriber_StreamingPull_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_streaming_pull_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_streaming_pull_sync.py new file mode 100644 index 000000000..0aa02fa40 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_streaming_pull_sync.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for StreamingPull +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_StreamingPull_sync] +from google import pubsub_v1 + + +def sample_streaming_pull(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.StreamingPullRequest( + subscription="subscription_value", + stream_ack_deadline_seconds=2813, + ) + + # This method expects an iterator which contains + # 'pubsub_v1.StreamingPullRequest' objects + # Here we create a generator that yields a single `request` for + # demonstrative purposes. + requests = [request] + + def request_generator(): + for request in requests: + yield request + + # Make the request + stream = client.streaming_pull(requests=request_generator()) + + # Handle the response + for response in stream: + print(response) + +# [END pubsub_v1_generated_Subscriber_StreamingPull_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_update_snapshot_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_update_snapshot_async.py new file mode 100644 index 000000000..f07bca1f5 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_update_snapshot_async.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for UpdateSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_UpdateSnapshot_async] +from google import pubsub_v1 + + +async def sample_update_snapshot(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + request = pubsub_v1.UpdateSnapshotRequest( + ) + + # Make the request + response = await client.update_snapshot(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_UpdateSnapshot_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_update_snapshot_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_update_snapshot_sync.py new file mode 100644 index 000000000..7afe32ec2 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_update_snapshot_sync.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for UpdateSnapshot +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_UpdateSnapshot_sync] +from google import pubsub_v1 + + +def sample_update_snapshot(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + request = pubsub_v1.UpdateSnapshotRequest( + ) + + # Make the request + response = client.update_snapshot(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_UpdateSnapshot_sync] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_update_subscription_async.py b/samples/generated_samples/pubsub_v1_generated_subscriber_update_subscription_async.py new file mode 100644 index 000000000..5a0410ec3 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_update_subscription_async.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for UpdateSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_UpdateSubscription_async] +from google import pubsub_v1 + + +async def sample_update_subscription(): + # Create a client + client = pubsub_v1.SubscriberAsyncClient() + + # Initialize request argument(s) + subscription = pubsub_v1.Subscription() + subscription.name = "name_value" + subscription.topic = "topic_value" + + request = pubsub_v1.UpdateSubscriptionRequest( + subscription=subscription, + ) + + # Make the request + response = await client.update_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_UpdateSubscription_async] diff --git a/samples/generated_samples/pubsub_v1_generated_subscriber_update_subscription_sync.py b/samples/generated_samples/pubsub_v1_generated_subscriber_update_subscription_sync.py new file mode 100644 index 000000000..75d6e8a95 --- /dev/null +++ b/samples/generated_samples/pubsub_v1_generated_subscriber_update_subscription_sync.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generated code. DO NOT EDIT! +# +# Snippet for UpdateSubscription +# NOTE: This snippet has been automatically generated for illustrative purposes only. +# It may require modifications to work in your environment. + +# To install the latest published package dependency, execute the following: +# python3 -m pip install google-cloud-pubsub + + +# [START pubsub_v1_generated_Subscriber_UpdateSubscription_sync] +from google import pubsub_v1 + + +def sample_update_subscription(): + # Create a client + client = pubsub_v1.SubscriberClient() + + # Initialize request argument(s) + subscription = pubsub_v1.Subscription() + subscription.name = "name_value" + subscription.topic = "topic_value" + + request = pubsub_v1.UpdateSubscriptionRequest( + subscription=subscription, + ) + + # Make the request + response = client.update_subscription(request=request) + + # Handle the response + print(response) + +# [END pubsub_v1_generated_Subscriber_UpdateSubscription_sync] diff --git a/samples/generated_samples/snippet_metadata_pubsub_v1.json b/samples/generated_samples/snippet_metadata_pubsub_v1.json new file mode 100644 index 000000000..17a344964 --- /dev/null +++ b/samples/generated_samples/snippet_metadata_pubsub_v1.json @@ -0,0 +1,2735 @@ +{ + "snippets": [ + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "CreateTopic" + } + }, + "file": "pubsub_v1_generated_publisher_create_topic_async.py", + "regionTag": "pubsub_v1_generated_Publisher_CreateTopic_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "CreateTopic" + } + }, + "file": "pubsub_v1_generated_publisher_create_topic_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_CreateTopic_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "DeleteTopic" + } + }, + "file": "pubsub_v1_generated_publisher_delete_topic_async.py", + "regionTag": "pubsub_v1_generated_Publisher_DeleteTopic_async", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "DeleteTopic" + } + }, + "file": "pubsub_v1_generated_publisher_delete_topic_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_DeleteTopic_sync", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "DetachSubscription" + } + }, + "file": "pubsub_v1_generated_publisher_detach_subscription_async.py", + "regionTag": "pubsub_v1_generated_Publisher_DetachSubscription_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "DetachSubscription" + } + }, + "file": "pubsub_v1_generated_publisher_detach_subscription_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_DetachSubscription_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "GetTopic" + } + }, + "file": "pubsub_v1_generated_publisher_get_topic_async.py", + "regionTag": "pubsub_v1_generated_Publisher_GetTopic_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "GetTopic" + } + }, + "file": "pubsub_v1_generated_publisher_get_topic_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_GetTopic_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "ListTopicSnapshots" + } + }, + "file": "pubsub_v1_generated_publisher_list_topic_snapshots_async.py", + "regionTag": "pubsub_v1_generated_Publisher_ListTopicSnapshots_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "ListTopicSnapshots" + } + }, + "file": "pubsub_v1_generated_publisher_list_topic_snapshots_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_ListTopicSnapshots_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "ListTopicSubscriptions" + } + }, + "file": "pubsub_v1_generated_publisher_list_topic_subscriptions_async.py", + "regionTag": "pubsub_v1_generated_Publisher_ListTopicSubscriptions_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "ListTopicSubscriptions" + } + }, + "file": "pubsub_v1_generated_publisher_list_topic_subscriptions_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_ListTopicSubscriptions_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "ListTopics" + } + }, + "file": "pubsub_v1_generated_publisher_list_topics_async.py", + "regionTag": "pubsub_v1_generated_Publisher_ListTopics_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "ListTopics" + } + }, + "file": "pubsub_v1_generated_publisher_list_topics_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_ListTopics_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "Publish" + } + }, + "file": "pubsub_v1_generated_publisher_publish_async.py", + "regionTag": "pubsub_v1_generated_Publisher_Publish_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "Publish" + } + }, + "file": "pubsub_v1_generated_publisher_publish_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_Publish_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "UpdateTopic" + } + }, + "file": "pubsub_v1_generated_publisher_update_topic_async.py", + "regionTag": "pubsub_v1_generated_Publisher_UpdateTopic_async", + "segments": [ + { + "end": 47, + "start": 27, + "type": "FULL" + }, + { + "end": 47, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 41, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 44, + "start": 42, + "type": "REQUEST_EXECUTION" + }, + { + "end": 48, + "start": 45, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Publisher" + }, + "shortName": "UpdateTopic" + } + }, + "file": "pubsub_v1_generated_publisher_update_topic_sync.py", + "regionTag": "pubsub_v1_generated_Publisher_UpdateTopic_sync", + "segments": [ + { + "end": 47, + "start": 27, + "type": "FULL" + }, + { + "end": 47, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 41, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 44, + "start": 42, + "type": "REQUEST_EXECUTION" + }, + { + "end": 48, + "start": 45, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "CreateSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_create_schema_async.py", + "regionTag": "pubsub_v1_generated_SchemaService_CreateSchema_async", + "segments": [ + { + "end": 48, + "start": 27, + "type": "FULL" + }, + { + "end": 48, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 42, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 45, + "start": 43, + "type": "REQUEST_EXECUTION" + }, + { + "end": 49, + "start": 46, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "CreateSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_create_schema_sync.py", + "regionTag": "pubsub_v1_generated_SchemaService_CreateSchema_sync", + "segments": [ + { + "end": 48, + "start": 27, + "type": "FULL" + }, + { + "end": 48, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 42, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 45, + "start": 43, + "type": "REQUEST_EXECUTION" + }, + { + "end": 49, + "start": 46, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "DeleteSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_delete_schema_async.py", + "regionTag": "pubsub_v1_generated_SchemaService_DeleteSchema_async", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "DeleteSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_delete_schema_sync.py", + "regionTag": "pubsub_v1_generated_SchemaService_DeleteSchema_sync", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "GetSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_get_schema_async.py", + "regionTag": "pubsub_v1_generated_SchemaService_GetSchema_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "GetSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_get_schema_sync.py", + "regionTag": "pubsub_v1_generated_SchemaService_GetSchema_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "ListSchemas" + } + }, + "file": "pubsub_v1_generated_schema_service_list_schemas_async.py", + "regionTag": "pubsub_v1_generated_SchemaService_ListSchemas_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "ListSchemas" + } + }, + "file": "pubsub_v1_generated_schema_service_list_schemas_sync.py", + "regionTag": "pubsub_v1_generated_SchemaService_ListSchemas_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "ValidateMessage" + } + }, + "file": "pubsub_v1_generated_schema_service_validate_message_async.py", + "regionTag": "pubsub_v1_generated_SchemaService_ValidateMessage_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "ValidateMessage" + } + }, + "file": "pubsub_v1_generated_schema_service_validate_message_sync.py", + "regionTag": "pubsub_v1_generated_SchemaService_ValidateMessage_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "ValidateSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_validate_schema_async.py", + "regionTag": "pubsub_v1_generated_SchemaService_ValidateSchema_async", + "segments": [ + { + "end": 48, + "start": 27, + "type": "FULL" + }, + { + "end": 48, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 42, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 45, + "start": 43, + "type": "REQUEST_EXECUTION" + }, + { + "end": 49, + "start": 46, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "SchemaService" + }, + "shortName": "ValidateSchema" + } + }, + "file": "pubsub_v1_generated_schema_service_validate_schema_sync.py", + "regionTag": "pubsub_v1_generated_SchemaService_ValidateSchema_sync", + "segments": [ + { + "end": 48, + "start": 27, + "type": "FULL" + }, + { + "end": 48, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 42, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 45, + "start": 43, + "type": "REQUEST_EXECUTION" + }, + { + "end": 49, + "start": 46, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "Acknowledge" + } + }, + "file": "pubsub_v1_generated_subscriber_acknowledge_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_Acknowledge_async", + "segments": [ + { + "end": 43, + "start": 27, + "type": "FULL" + }, + { + "end": 43, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 44, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "Acknowledge" + } + }, + "file": "pubsub_v1_generated_subscriber_acknowledge_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_Acknowledge_sync", + "segments": [ + { + "end": 43, + "start": 27, + "type": "FULL" + }, + { + "end": 43, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 44, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "CreateSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_create_snapshot_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_CreateSnapshot_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "CreateSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_create_snapshot_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_CreateSnapshot_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "CreateSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_create_subscription_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_CreateSubscription_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "CreateSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_create_subscription_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_CreateSubscription_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "DeleteSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_delete_snapshot_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_DeleteSnapshot_async", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "DeleteSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_delete_snapshot_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_DeleteSnapshot_sync", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "DeleteSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_delete_subscription_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_DeleteSubscription_async", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "DeleteSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_delete_subscription_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_DeleteSubscription_sync", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "GetSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_get_snapshot_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_GetSnapshot_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "GetSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_get_snapshot_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_GetSnapshot_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "GetSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_get_subscription_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_GetSubscription_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "GetSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_get_subscription_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_GetSubscription_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ListSnapshots" + } + }, + "file": "pubsub_v1_generated_subscriber_list_snapshots_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_ListSnapshots_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ListSnapshots" + } + }, + "file": "pubsub_v1_generated_subscriber_list_snapshots_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_ListSnapshots_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ListSubscriptions" + } + }, + "file": "pubsub_v1_generated_subscriber_list_subscriptions_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_ListSubscriptions_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ListSubscriptions" + } + }, + "file": "pubsub_v1_generated_subscriber_list_subscriptions_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_ListSubscriptions_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ModifyAckDeadline" + } + }, + "file": "pubsub_v1_generated_subscriber_modify_ack_deadline_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_ModifyAckDeadline_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 40, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 41, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ModifyAckDeadline" + } + }, + "file": "pubsub_v1_generated_subscriber_modify_ack_deadline_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_ModifyAckDeadline_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 40, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 41, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ModifyPushConfig" + } + }, + "file": "pubsub_v1_generated_subscriber_modify_push_config_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_ModifyPushConfig_async", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "ModifyPushConfig" + } + }, + "file": "pubsub_v1_generated_subscriber_modify_push_config_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_ModifyPushConfig_sync", + "segments": [ + { + "end": 42, + "start": 27, + "type": "FULL" + }, + { + "end": 42, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "Pull" + } + }, + "file": "pubsub_v1_generated_subscriber_pull_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_Pull_async", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "Pull" + } + }, + "file": "pubsub_v1_generated_subscriber_pull_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_Pull_sync", + "segments": [ + { + "end": 45, + "start": 27, + "type": "FULL" + }, + { + "end": 45, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 39, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 42, + "start": 40, + "type": "REQUEST_EXECUTION" + }, + { + "end": 46, + "start": 43, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "Seek" + } + }, + "file": "pubsub_v1_generated_subscriber_seek_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_Seek_async", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "Seek" + } + }, + "file": "pubsub_v1_generated_subscriber_seek_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_Seek_sync", + "segments": [ + { + "end": 44, + "start": 27, + "type": "FULL" + }, + { + "end": 44, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 38, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 41, + "start": 39, + "type": "REQUEST_EXECUTION" + }, + { + "end": 45, + "start": 42, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "StreamingPull" + } + }, + "file": "pubsub_v1_generated_subscriber_streaming_pull_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_StreamingPull_async", + "segments": [ + { + "end": 56, + "start": 27, + "type": "FULL" + }, + { + "end": 56, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 49, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 52, + "start": 50, + "type": "REQUEST_EXECUTION" + }, + { + "end": 57, + "start": 53, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "StreamingPull" + } + }, + "file": "pubsub_v1_generated_subscriber_streaming_pull_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_StreamingPull_sync", + "segments": [ + { + "end": 56, + "start": 27, + "type": "FULL" + }, + { + "end": 56, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 49, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 52, + "start": 50, + "type": "REQUEST_EXECUTION" + }, + { + "end": 57, + "start": 53, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "UpdateSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_update_snapshot_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_UpdateSnapshot_async", + "segments": [ + { + "end": 43, + "start": 27, + "type": "FULL" + }, + { + "end": 43, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 37, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 40, + "start": 38, + "type": "REQUEST_EXECUTION" + }, + { + "end": 44, + "start": 41, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "UpdateSnapshot" + } + }, + "file": "pubsub_v1_generated_subscriber_update_snapshot_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_UpdateSnapshot_sync", + "segments": [ + { + "end": 43, + "start": 27, + "type": "FULL" + }, + { + "end": 43, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 37, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 40, + "start": 38, + "type": "REQUEST_EXECUTION" + }, + { + "end": 44, + "start": 41, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "async": true, + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "UpdateSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_update_subscription_async.py", + "regionTag": "pubsub_v1_generated_Subscriber_UpdateSubscription_async", + "segments": [ + { + "end": 48, + "start": 27, + "type": "FULL" + }, + { + "end": 48, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 42, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 45, + "start": 43, + "type": "REQUEST_EXECUTION" + }, + { + "end": 49, + "start": 46, + "type": "RESPONSE_HANDLING" + } + ] + }, + { + "clientMethod": { + "method": { + "service": { + "shortName": "Subscriber" + }, + "shortName": "UpdateSubscription" + } + }, + "file": "pubsub_v1_generated_subscriber_update_subscription_sync.py", + "regionTag": "pubsub_v1_generated_Subscriber_UpdateSubscription_sync", + "segments": [ + { + "end": 48, + "start": 27, + "type": "FULL" + }, + { + "end": 48, + "start": 27, + "type": "SHORT" + }, + { + "end": 33, + "start": 31, + "type": "CLIENT_INITIALIZATION" + }, + { + "end": 42, + "start": 34, + "type": "REQUEST_INITIALIZATION" + }, + { + "end": 45, + "start": 43, + "type": "REQUEST_EXECUTION" + }, + { + "end": 49, + "start": 46, + "type": "RESPONSE_HANDLING" + } + ] + } + ] +} diff --git a/samples/snippets/iam_test.py b/samples/snippets/iam_test.py index f5d0ef192..655e43e36 100644 --- a/samples/snippets/iam_test.py +++ b/samples/snippets/iam_test.py @@ -78,14 +78,14 @@ def subscription_path( pass -def test_get_topic_policy(topic_path: str, capsys: CaptureFixture) -> None: +def test_get_topic_policy(topic_path: str, capsys: CaptureFixture[str]) -> None: iam.get_topic_policy(PROJECT_ID, TOPIC_ID) out, _ = capsys.readouterr() assert topic_path in out def test_get_subscription_policy( - subscription_path: str, capsys: CaptureFixture + subscription_path: str, capsys: CaptureFixture[str] ) -> None: iam.get_subscription_policy(PROJECT_ID, SUBSCRIPTION_ID) out, _ = capsys.readouterr() @@ -93,8 +93,8 @@ def test_get_subscription_policy( def test_set_topic_policy( - publisher_client: pubsub_v1.PublisherClient, topic_path: str, -) -> CaptureFixture: + publisher_client: pubsub_v1.PublisherClient, topic_path: str +) -> None: iam.set_topic_policy(PROJECT_ID, TOPIC_ID) policy = publisher_client.get_iam_policy(request={"resource": topic_path}) assert "roles/pubsub.publisher" in str(policy) @@ -110,7 +110,7 @@ def test_set_subscription_policy( assert "domain:google.com" in str(policy) -def test_check_topic_permissions(topic_path: str, capsys: CaptureFixture) -> None: +def test_check_topic_permissions(topic_path: str, capsys: CaptureFixture[str]) -> None: iam.check_topic_permissions(PROJECT_ID, TOPIC_ID) out, _ = capsys.readouterr() assert topic_path in out @@ -118,7 +118,7 @@ def test_check_topic_permissions(topic_path: str, capsys: CaptureFixture) -> Non def test_check_subscription_permissions( - subscription_path: str, capsys: CaptureFixture, + subscription_path: str, capsys: CaptureFixture[str], ) -> None: iam.check_subscription_permissions(PROJECT_ID, SUBSCRIPTION_ID) out, _ = capsys.readouterr() diff --git a/samples/snippets/mypy.ini b/samples/snippets/mypy.ini new file mode 100644 index 000000000..8f2bae69a --- /dev/null +++ b/samples/snippets/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +; We require type annotations in all samples. +strict = True +exclude = noxfile\.py +warn_unused_configs = True + +[mypy-avro.*,backoff,flaky] +ignore_missing_imports = True diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index 93a9122cc..20cdfc620 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -14,6 +14,7 @@ from __future__ import print_function +import glob import os from pathlib import Path import sys @@ -184,37 +185,45 @@ def blacken(session: nox.sessions.Session) -> None: def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: - if TEST_CONFIG["pip_version_override"]: - pip_version = TEST_CONFIG["pip_version_override"] - session.install(f"pip=={pip_version}") - """Runs py.test for a particular project.""" - if os.path.exists("requirements.txt"): - if os.path.exists("constraints.txt"): - session.install("-r", "requirements.txt", "-c", "constraints.txt") - else: - session.install("-r", "requirements.txt") - - if os.path.exists("requirements-test.txt"): - if os.path.exists("constraints-test.txt"): - session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") - else: - session.install("-r", "requirements-test.txt") - - if INSTALL_LIBRARY_FROM_SOURCE: - session.install("-e", _get_repo_root()) - - if post_install: - post_install(session) - - session.run( - "pytest", - *(PYTEST_COMMON_ARGS + session.posargs), - # Pytest will return 5 when no tests are collected. This can happen - # on travis where slow and flaky tests are excluded. - # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html - success_codes=[0, 5], - env=get_pytest_env_vars(), - ) + # check for presence of tests + test_list = glob.glob("*_test.py") + glob.glob("test_*.py") + test_list.extend(glob.glob("tests")) + if len(test_list) == 0: + print("No tests found, skipping directory.") + else: + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) @nox.session(python=ALL_VERSIONS) diff --git a/samples/snippets/publisher.py b/samples/snippets/publisher.py index 821efcbb5..d6e520772 100644 --- a/samples/snippets/publisher.py +++ b/samples/snippets/publisher.py @@ -95,9 +95,9 @@ def publish_messages(project_id: str, topic_id: str) -> None: topic_path = publisher.topic_path(project_id, topic_id) for n in range(1, 10): - data = f"Message number {n}" + data_str = f"Message number {n}" # Data must be a bytestring - data = data.encode("utf-8") + data = data_str.encode("utf-8") # When you publish a message, the client returns a future. future = publisher.publish(topic_path, data) print(future.result()) @@ -121,9 +121,9 @@ def publish_messages_with_custom_attributes(project_id: str, topic_id: str) -> N topic_path = publisher.topic_path(project_id, topic_id) for n in range(1, 10): - data = f"Message number {n}" + data_str = f"Message number {n}" # Data must be a bytestring - data = data.encode("utf-8") + data = data_str.encode("utf-8") # Add two attributes, origin and username, to the message future = publisher.publish( topic_path, data, origin="python-sample", username="gcp" @@ -189,7 +189,7 @@ def publish_messages_with_batch_settings(project_id: str, topic_id: str) -> None # or 1 KiB of data, or 1 second has passed. batch_settings = pubsub_v1.types.BatchSettings( max_messages=10, # default 100 - max_bytes=1024, # default 1 MiB + max_bytes=1024, # default 1 MB max_latency=1, # default 10 ms ) publisher = pubsub_v1.PublisherClient(batch_settings) @@ -202,9 +202,9 @@ def callback(future: pubsub_v1.publisher.futures.Future) -> None: print(message_id) for n in range(1, 10): - data = f"Message number {n}" + data_str = f"Message number {n}" # Data must be a bytestring - data = data.encode("utf-8") + data = data_str.encode("utf-8") publish_future = publisher.publish(topic_path, data) # Non-blocking. Allow the publisher client to batch multiple messages. publish_future.add_done_callback(callback) @@ -252,9 +252,9 @@ def callback(publish_future: pubsub_v1.publisher.futures.Future) -> None: # Publish 1000 messages in quick succession may be constrained by # publisher flow control. for n in range(1, 1000): - data = f"Message number {n}" + data_str = f"Message number {n}" # Data must be a bytestring - data = data.encode("utf-8") + data = data_str.encode("utf-8") publish_future = publisher.publish(topic_path, data) # Non-blocking. Allow the publisher client to batch messages. publish_future.add_done_callback(callback) @@ -298,9 +298,9 @@ def publish_messages_with_retry_settings(project_id: str, topic_id: str) -> None topic_path = publisher.topic_path(project_id, topic_id) for n in range(1, 10): - data = f"Message number {n}" + data_str = f"Message number {n}" # Data must be a bytestring - data = data.encode("utf-8") + data = data_str.encode("utf-8") future = publisher.publish(topic=topic_path, data=data, retry=custom_retry) print(future.result()) diff --git a/samples/snippets/publisher_test.py b/samples/snippets/publisher_test.py index 0e06d8f2a..cf00da98e 100644 --- a/samples/snippets/publisher_test.py +++ b/samples/snippets/publisher_test.py @@ -14,7 +14,8 @@ import os import time -from typing import Generator +import typing +from typing import Any, Callable, cast, Iterator, TypeVar, Union import uuid from _pytest.capture import CaptureFixture @@ -26,6 +27,7 @@ import publisher + UUID = uuid.uuid4().hex PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] TOPIC_ID = "publisher-test-topic-" + UUID @@ -33,14 +35,19 @@ # Allow 60s for tests to finish. MAX_TIME = 60 +if typing.TYPE_CHECKING: + from unittest.mock import AsyncMock, MagicMock + + MockType = Union[MagicMock, AsyncMock] + @pytest.fixture(scope="module") -def publisher_client() -> Generator[pubsub_v1.PublisherClient, None, None]: +def publisher_client() -> Iterator[pubsub_v1.PublisherClient]: yield pubsub_v1.PublisherClient() @pytest.fixture(scope="module") -def subscriber_client() -> Generator[pubsub_v1.SubscriberClient, None, None]: +def subscriber_client() -> Iterator[pubsub_v1.SubscriberClient]: subscriber_client = pubsub_v1.SubscriberClient() yield subscriber_client # Close the subscriber client properly during teardown. @@ -48,9 +55,7 @@ def subscriber_client() -> Generator[pubsub_v1.SubscriberClient, None, None]: @pytest.fixture(scope="module") -def topic_path( - publisher_client: pubsub_v1.PublisherClient, -) -> Generator[str, None, None]: +def topic_path(publisher_client: pubsub_v1.PublisherClient) -> Iterator[str]: topic_path = publisher_client.topic_path(PROJECT_ID, TOPIC_ID) try: @@ -69,7 +74,7 @@ def topic_path( @pytest.fixture(scope="module") def subscription_path( subscriber_client: pubsub_v1.SubscriberClient, topic_path: str -) -> Generator[str, None, None]: +) -> Iterator[str]: subscription_path = subscriber_client.subscription_path(PROJECT_ID, SUBSCRIPTION_ID) subscription = subscriber_client.create_subscription( request={"name": subscription_path, "topic": topic_path} @@ -84,7 +89,7 @@ def subscription_path( pass -def _make_sleep_patch() -> None: +def _make_sleep_patch() -> 'mock.mock._patch["MockType"]': real_sleep = time.sleep def new_sleep(period: float) -> None: @@ -98,7 +103,7 @@ def new_sleep(period: float) -> None: def test_create( - publisher_client: pubsub_v1.PublisherClient, capsys: CaptureFixture + publisher_client: pubsub_v1.PublisherClient, capsys: CaptureFixture[str] ) -> None: # The scope of `topic_path` is limited to this function. topic_path = publisher_client.topic_path(PROJECT_ID, TOPIC_ID) @@ -114,14 +119,14 @@ def test_create( assert f"Created topic: {topic_path}" in out -def test_list(topic_path: str, capsys: CaptureFixture) -> None: +def test_list(topic_path: str, capsys: CaptureFixture[str]) -> None: publisher.list_topics(PROJECT_ID) out, _ = capsys.readouterr() assert topic_path in out -def test_publish(topic_path: str, capsys: CaptureFixture) -> None: +def test_publish(topic_path: str, capsys: CaptureFixture[str]) -> None: publisher.publish_messages(PROJECT_ID, TOPIC_ID) out, _ = capsys.readouterr() @@ -129,7 +134,7 @@ def test_publish(topic_path: str, capsys: CaptureFixture) -> None: def test_publish_with_custom_attributes( - topic_path: str, capsys: CaptureFixture + topic_path: str, capsys: CaptureFixture[str] ) -> None: publisher.publish_messages_with_custom_attributes(PROJECT_ID, TOPIC_ID) @@ -137,7 +142,9 @@ def test_publish_with_custom_attributes( assert f"Published messages with custom attributes to {topic_path}." in out -def test_publish_with_batch_settings(topic_path: str, capsys: CaptureFixture) -> None: +def test_publish_with_batch_settings( + topic_path: str, capsys: CaptureFixture[str] +) -> None: publisher.publish_messages_with_batch_settings(PROJECT_ID, TOPIC_ID) out, _ = capsys.readouterr() @@ -145,7 +152,7 @@ def test_publish_with_batch_settings(topic_path: str, capsys: CaptureFixture) -> def test_publish_with_flow_control_settings( - topic_path: str, capsys: CaptureFixture + topic_path: str, capsys: CaptureFixture[str] ) -> None: publisher.publish_messages_with_flow_control_settings(PROJECT_ID, TOPIC_ID) @@ -153,21 +160,27 @@ def test_publish_with_flow_control_settings( assert f"Published messages with flow control settings to {topic_path}." in out -def test_publish_with_retry_settings(topic_path: str, capsys: CaptureFixture) -> None: +def test_publish_with_retry_settings( + topic_path: str, capsys: CaptureFixture[str] +) -> None: publisher.publish_messages_with_retry_settings(PROJECT_ID, TOPIC_ID) out, _ = capsys.readouterr() assert f"Published messages with retry settings to {topic_path}." in out -def test_publish_with_error_handler(topic_path: str, capsys: CaptureFixture) -> None: +def test_publish_with_error_handler( + topic_path: str, capsys: CaptureFixture[str] +) -> None: publisher.publish_messages_with_error_handler(PROJECT_ID, TOPIC_ID) out, _ = capsys.readouterr() assert f"Published messages with error handler to {topic_path}." in out -def test_publish_with_ordering_keys(topic_path: str, capsys: CaptureFixture) -> None: +def test_publish_with_ordering_keys( + topic_path: str, capsys: CaptureFixture[str] +) -> None: publisher.publish_with_ordering_keys(PROJECT_ID, TOPIC_ID) out, _ = capsys.readouterr() @@ -175,7 +188,7 @@ def test_publish_with_ordering_keys(topic_path: str, capsys: CaptureFixture) -> def test_resume_publish_with_error_handler( - topic_path: str, capsys: CaptureFixture + topic_path: str, capsys: CaptureFixture[str] ) -> None: publisher.resume_publish_with_ordering_keys(PROJECT_ID, TOPIC_ID) @@ -183,7 +196,9 @@ def test_resume_publish_with_error_handler( assert f"Resumed publishing messages with ordering keys to {topic_path}." in out -def test_detach_subscription(subscription_path: str, capsys: CaptureFixture) -> None: +def test_detach_subscription( + subscription_path: str, capsys: CaptureFixture[str] +) -> None: publisher.detach_subscription(PROJECT_ID, SUBSCRIPTION_ID) out, _ = capsys.readouterr() @@ -193,7 +208,14 @@ def test_detach_subscription(subscription_path: str, capsys: CaptureFixture) -> def test_delete(publisher_client: pubsub_v1.PublisherClient) -> None: publisher.delete_topic(PROJECT_ID, TOPIC_ID) - @backoff.on_exception(backoff.expo, AssertionError, max_time=MAX_TIME) + C = TypeVar("C", bound=Callable[..., Any]) + + typed_backoff = cast( + Callable[[C], C], + backoff.on_exception(backoff.expo, AssertionError, max_time=MAX_TIME), + ) + + @typed_backoff def eventually_consistent_test() -> None: with pytest.raises(Exception): publisher_client.get_topic( diff --git a/samples/snippets/quickstart/pub.py b/samples/snippets/quickstart/pub.py index 80bf157a3..7215abd86 100644 --- a/samples/snippets/quickstart/pub.py +++ b/samples/snippets/quickstart/pub.py @@ -33,7 +33,7 @@ def pub(project_id: str, topic_id: str) -> None: api_future = client.publish(topic_path, data) message_id = api_future.result() - print(f"Published {data} to {topic_path}: {message_id}") + print(f"Published {data.decode()} to {topic_path}: {message_id}") if __name__ == "__main__": diff --git a/samples/snippets/quickstart/quickstart_test.py b/samples/snippets/quickstart/quickstart_test.py index e2a5e9844..3ed07cf81 100644 --- a/samples/snippets/quickstart/quickstart_test.py +++ b/samples/snippets/quickstart/quickstart_test.py @@ -15,7 +15,7 @@ # limitations under the License. import os -from typing import Generator +from typing import Any, Callable, cast, Iterator, TypeVar import uuid from _pytest.capture import CaptureFixture @@ -32,19 +32,19 @@ @pytest.fixture(scope="module") -def publisher_client() -> Generator[pubsub_v1.PublisherClient, None, None]: +def publisher_client() -> Iterator[pubsub_v1.PublisherClient]: yield pubsub_v1.PublisherClient() @pytest.fixture(scope="module") -def subscriber_client() -> Generator[pubsub_v1.SubscriberClient, None, None]: +def subscriber_client() -> Iterator[pubsub_v1.SubscriberClient]: subscriber_client = pubsub_v1.SubscriberClient() yield subscriber_client subscriber_client.close() @pytest.fixture(scope="module") -def topic_path(publisher_client: pubsub_v1.PublisherClient) -> Generator[str, None, None]: +def topic_path(publisher_client: pubsub_v1.PublisherClient) -> Iterator[str]: topic_path = publisher_client.topic_path(PROJECT_ID, TOPIC_ID) try: @@ -57,7 +57,9 @@ def topic_path(publisher_client: pubsub_v1.PublisherClient) -> Generator[str, No @pytest.fixture(scope="module") -def subscription_path(subscriber_client: pubsub_v1.SubscriberClient, topic_path: str) -> Generator[str, None, None]: +def subscription_path( + subscriber_client: pubsub_v1.SubscriberClient, topic_path: str +) -> Iterator[str]: subscription_path = subscriber_client.subscription_path(PROJECT_ID, SUBSCRIPTION_ID) try: @@ -72,7 +74,7 @@ def subscription_path(subscriber_client: pubsub_v1.SubscriberClient, topic_path: subscriber_client.close() -def test_pub(topic_path: str, capsys: CaptureFixture) -> None: +def test_pub(topic_path: str, capsys: CaptureFixture[str]) -> None: import pub pub.pub(PROJECT_ID, TOPIC_ID) @@ -82,8 +84,17 @@ def test_pub(topic_path: str, capsys: CaptureFixture) -> None: assert "Hello, World!" in out -@flaky(max_runs=3, min_passes=1) -def test_sub(publisher_client: pubsub_v1.PublisherClient, topic_path: str, subscription_path: str, capsys: CaptureFixture) -> None: +C = TypeVar("C", bound=Callable[..., Any]) +_typed_flaky = cast(Callable[[C], C], flaky(max_runs=3, min_passes=1)) + + +@_typed_flaky +def test_sub( + publisher_client: pubsub_v1.PublisherClient, + topic_path: str, + subscription_path: str, + capsys: CaptureFixture[str], +) -> None: publisher_client.publish(topic_path, b"Hello World!") import sub diff --git a/samples/snippets/quickstart/sub.py b/samples/snippets/quickstart/sub.py index d3326f980..0900f652d 100644 --- a/samples/snippets/quickstart/sub.py +++ b/samples/snippets/quickstart/sub.py @@ -15,11 +15,12 @@ # limitations under the License. import argparse +from typing import Optional from google.cloud import pubsub_v1 -def sub(project_id: str, subscription_id: str, timeout: float = None) -> None: +def sub(project_id: str, subscription_id: str, timeout: Optional[float] = None) -> None: """Receives messages from a Pub/Sub subscription.""" # Initialize a Subscriber client subscriber_client = pubsub_v1.SubscriberClient() diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index b1b513af7..1fc2a0443 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -1,4 +1,4 @@ backoff==1.11.1 -pytest==6.2.5 +pytest==7.0.1 mock==4.0.3 flaky==3.7.0 \ No newline at end of file diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index 40343160b..40078e73f 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-pubsub==2.8.0 +google-cloud-pubsub==2.9.0 avro==1.11.0 diff --git a/samples/snippets/schema.py b/samples/snippets/schema.py index bf7ae3fbe..977e4c0c4 100644 --- a/samples/snippets/schema.py +++ b/samples/snippets/schema.py @@ -22,6 +22,7 @@ """ import argparse +from typing import Optional from google.cloud import pubsub_v1 @@ -234,10 +235,11 @@ def publish_avro_records(project_id: str, topic_id: str, avsc_file: str) -> None encoder = BinaryEncoder(bout) writer.write(record, encoder) data = bout.getvalue() - print(f"Preparing a binary-encoded message:\n{data}") + print(f"Preparing a binary-encoded message:\n{data.decode()}") elif encoding == Encoding.JSON: - data = json.dumps(record).encode("utf-8") - print(f"Preparing a JSON-encoded message:\n{data}") + data_str = json.dumps(record) + print(f"Preparing a JSON-encoded message:\n{data_str}") + data = data_str.encode("utf-8") else: print(f"No encoding specified in {topic_path}. Abort.") exit(0) @@ -258,7 +260,7 @@ def publish_proto_messages(project_id: str, topic_id: str) -> None: from google.protobuf.json_format import MessageToJson from google.pubsub_v1.types import Encoding - from utilities import us_states_pb2 + from utilities import us_states_pb2 # type: ignore # TODO(developer): Replace these variables before running the sample. # project_id = "your-project-id" @@ -298,7 +300,10 @@ def publish_proto_messages(project_id: str, topic_id: str) -> None: def subscribe_with_avro_schema( - project_id: str, subscription_id: str, avsc_file: str, timeout: float = None + project_id: str, + subscription_id: str, + avsc_file: str, + timeout: Optional[float] = None, ) -> None: """Receive and decode messages sent to a topic with an Avro schema.""" # [START pubsub_subscribe_avro_records] diff --git a/samples/snippets/schema_test.py b/samples/snippets/schema_test.py index 1f74c5eb3..2cdf4bfb6 100644 --- a/samples/snippets/schema_test.py +++ b/samples/snippets/schema_test.py @@ -15,7 +15,7 @@ # limitations under the License. import os -from typing import Generator +from typing import Any, Callable, cast, Generator, TypeVar import uuid from _pytest.capture import CaptureFixture @@ -193,7 +193,7 @@ def proto_subscription( def test_create_avro_schema( schema_client: pubsub_v1.SchemaServiceClient, avro_schema: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: try: schema_client.delete_schema(request={"name": avro_schema}) @@ -210,7 +210,7 @@ def test_create_avro_schema( def test_create_proto_schema( schema_client: pubsub_v1.SchemaServiceClient, proto_schema: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: try: schema_client.delete_schema(request={"name": proto_schema}) @@ -224,20 +224,22 @@ def test_create_proto_schema( assert f"{proto_schema}" in out -def test_get_schema(avro_schema: str, capsys: CaptureFixture) -> None: +def test_get_schema(avro_schema: str, capsys: CaptureFixture[str]) -> None: schema.get_schema(PROJECT_ID, AVRO_SCHEMA_ID) out, _ = capsys.readouterr() assert "Got a schema" in out assert f"{avro_schema}" in out -def test_list_schemas(capsys: CaptureFixture) -> None: +def test_list_schemas(capsys: CaptureFixture[str]) -> None: schema.list_schemas(PROJECT_ID) out, _ = capsys.readouterr() assert "Listed schemas." in out -def test_create_topic_with_schema(avro_schema: str, capsys: CaptureFixture) -> None: +def test_create_topic_with_schema( + avro_schema: str, capsys: CaptureFixture[str] +) -> None: schema.create_topic_with_schema(PROJECT_ID, AVRO_TOPIC_ID, AVRO_SCHEMA_ID, "BINARY") out, _ = capsys.readouterr() assert "Created a topic" in out @@ -247,7 +249,7 @@ def test_create_topic_with_schema(avro_schema: str, capsys: CaptureFixture) -> N def test_publish_avro_records( - avro_schema: str, avro_topic: str, capsys: CaptureFixture + avro_schema: str, avro_topic: str, capsys: CaptureFixture[str] ) -> None: schema.publish_avro_records(PROJECT_ID, AVRO_TOPIC_ID, AVSC_FILE) out, _ = capsys.readouterr() @@ -256,7 +258,10 @@ def test_publish_avro_records( def test_subscribe_with_avro_schema( - avro_schema: str, avro_topic: str, avro_subscription: str, capsys: CaptureFixture + avro_schema: str, + avro_topic: str, + avro_subscription: str, + capsys: CaptureFixture[str], ) -> None: schema.publish_avro_records(PROJECT_ID, AVRO_TOPIC_ID, AVSC_FILE) @@ -265,7 +270,7 @@ def test_subscribe_with_avro_schema( assert "Received a binary-encoded message:" in out -def test_publish_proto_records(proto_topic: str, capsys: CaptureFixture) -> None: +def test_publish_proto_records(proto_topic: str, capsys: CaptureFixture[str]) -> None: schema.publish_proto_messages(PROJECT_ID, PROTO_TOPIC_ID) out, _ = capsys.readouterr() assert "Preparing a binary-encoded message" in out @@ -273,7 +278,10 @@ def test_publish_proto_records(proto_topic: str, capsys: CaptureFixture) -> None def test_subscribe_with_proto_schema( - proto_schema: str, proto_topic: str, proto_subscription: str, capsys: CaptureFixture + proto_schema: str, + proto_topic: str, + proto_subscription: str, + capsys: CaptureFixture[str], ) -> None: schema.publish_proto_messages(PROJECT_ID, PROTO_TOPIC_ID) @@ -282,8 +290,12 @@ def test_subscribe_with_proto_schema( assert "Received a binary-encoded message" in out -@flaky(max_runs=3, min_passes=1) -def test_delete_schema(proto_schema: str, capsys: CaptureFixture) -> None: +C = TypeVar("C", bound=Callable[..., Any]) +typed_flaky = cast(Callable[[C], C], flaky(max_runs=3, min_passes=1)) + + +@typed_flaky +def test_delete_schema(proto_schema: str, capsys: CaptureFixture[str]) -> None: schema.delete_schema(PROJECT_ID, PROTO_SCHEMA_ID) out, _ = capsys.readouterr() assert "Deleted a schema" in out diff --git a/samples/snippets/subscriber.py b/samples/snippets/subscriber.py index 0c67c95fb..5a9d0a7a5 100644 --- a/samples/snippets/subscriber.py +++ b/samples/snippets/subscriber.py @@ -22,6 +22,11 @@ """ import argparse +import typing +from typing import Optional + +if typing.TYPE_CHECKING: + from google.pubsub_v1 import types as gapic_types def list_subscriptions_in_topic(project_id: str, topic_id: str) -> None: @@ -185,7 +190,7 @@ def create_push_subscription( def create_subscription_with_ordering( project_id: str, topic_id: str, subscription_id: str ) -> None: - """Create a subscription with dead letter policy.""" + """Create a subscription with ordering enabled.""" # [START pubsub_enable_subscription_ordering] from google.cloud import pubsub_v1 @@ -211,6 +216,63 @@ def create_subscription_with_ordering( # [END pubsub_enable_subscription_ordering] +def create_subscription_with_filtering( + project_id: str, topic_id: str, subscription_id: str, filter: str, +) -> None: + """Create a subscription with filtering enabled.""" + # [START pubsub_create_subscription_with_filter] + from google.cloud import pubsub_v1 + + # TODO(developer): Choose an existing topic. + # project_id = "your-project-id" + # topic_id = "your-topic-id" + # subscription_id = "your-subscription-id" + # filter = "attributes.author=\"unknown\"" + + publisher = pubsub_v1.PublisherClient() + subscriber = pubsub_v1.SubscriberClient() + topic_path = publisher.topic_path(project_id, topic_id) + subscription_path = subscriber.subscription_path(project_id, subscription_id) + + with subscriber: + subscription = subscriber.create_subscription( + request={"name": subscription_path, "topic": topic_path, "filter": filter} + ) + print(f"Created subscription with filtering enabled: {subscription}") + # [END pubsub_create_subscription_with_filter] + + +def create_subscription_with_exactly_once_delivery( + project_id: str, topic_id: str, subscription_id: str +) -> None: + """Create a subscription with exactly once delivery enabled.""" + # [START pubsub_create_subscription_with_exactly_once_delivery] + from google.cloud import pubsub_v1 + + # TODO(developer): Choose an existing topic. + # project_id = "your-project-id" + # topic_id = "your-topic-id" + # subscription_id = "your-subscription-id" + + publisher = pubsub_v1.PublisherClient() + subscriber = pubsub_v1.SubscriberClient() + topic_path = publisher.topic_path(project_id, topic_id) + subscription_path = subscriber.subscription_path(project_id, subscription_id) + + with subscriber: + subscription = subscriber.create_subscription( + request={ + "name": subscription_path, + "topic": topic_path, + "enable_exactly_once_delivery": True, + } + ) + print( + f"Created subscription with exactly once delivery enabled: {subscription}" + ) + # [END pubsub_create_subscription_with_exactly_once_delivery] + + def delete_subscription(project_id: str, subscription_id: str) -> None: """Deletes an existing Pub/Sub topic.""" # [START pubsub_delete_subscription] @@ -278,7 +340,7 @@ def update_subscription_with_dead_letter_policy( subscription_id: str, dead_letter_topic_id: str, max_delivery_attempts: int = 5, -) -> None: +) -> "gapic_types.Subscription": """Update a subscription's dead letter policy.""" # [START pubsub_dead_letter_update_subscription] from google.cloud import pubsub_v1 @@ -338,7 +400,7 @@ def update_subscription_with_dead_letter_policy( def remove_dead_letter_policy( project_id: str, topic_id: str, subscription_id: str -) -> None: +) -> "gapic_types.Subscription": """Remove dead letter policy from a subscription.""" # [START pubsub_dead_letter_remove] from google.cloud import pubsub_v1 @@ -382,7 +444,7 @@ def remove_dead_letter_policy( def receive_messages( - project_id: str, subscription_id: str, timeout: float = None + project_id: str, subscription_id: str, timeout: Optional[float] = None ) -> None: """Receives messages from a pull subscription.""" # [START pubsub_subscriber_async_pull] @@ -422,7 +484,7 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: def receive_messages_with_custom_attributes( - project_id: str, subscription_id: str, timeout: float = None + project_id: str, subscription_id: str, timeout: Optional[float] = None ) -> None: """Receives messages from a pull subscription.""" # [START pubsub_subscriber_async_pull_custom_attributes] @@ -439,7 +501,7 @@ def receive_messages_with_custom_attributes( subscription_path = subscriber.subscription_path(project_id, subscription_id) def callback(message: pubsub_v1.subscriber.message.Message) -> None: - print(f"Received {message.data}.") + print(f"Received {message.data!r}.") if message.attributes: print("Attributes:") for key in message.attributes: @@ -463,7 +525,7 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: def receive_messages_with_flow_control( - project_id: str, subscription_id: str, timeout: float = None + project_id: str, subscription_id: str, timeout: Optional[float] = None ) -> None: """Receives messages from a pull subscription with flow control.""" # [START pubsub_subscriber_flow_settings] @@ -480,7 +542,7 @@ def receive_messages_with_flow_control( subscription_path = subscriber.subscription_path(project_id, subscription_id) def callback(message: pubsub_v1.subscriber.message.Message) -> None: - print(f"Received {message.data}.") + print(f"Received {message.data!r}.") message.ack() # Limit the subscriber to only have ten outstanding messages at a time. @@ -522,10 +584,10 @@ def receive_messages_with_blocking_shutdown( subscription_path = subscriber.subscription_path(project_id, subscription_id) def callback(message: pubsub_v1.subscriber.message.Message) -> None: - print(f"Received {message.data}.") + print(f"Received {message.data!r}.") time.sleep(timeout + 3.0) # Pocess longer than streaming pull future timeout. message.ack() - print(f"Done processing the message {message.data}.") + print(f"Done processing the message {message.data!r}.") streaming_pull_future = subscriber.subscribe( subscription_path, callback=callback, await_callbacks_on_shutdown=True, @@ -549,6 +611,61 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: # [END pubsub_subscriber_blocking_shutdown] +def receive_messages_with_exactly_once_delivery_enabled( + project_id: str, subscription_id: str, timeout: Optional[float] = None +) -> None: + """Receives messages from a pull subscription with exactly-once delivery enabled.""" + # [START pubsub_subscriber_exactly_once] + from concurrent.futures import TimeoutError + from google.cloud import pubsub_v1 + from google.cloud.pubsub_v1.subscriber import exceptions as sub_exceptions + + # TODO(developer) + # project_id = "your-project-id" + # subscription_id = "your-subscription-id" + # Number of seconds the subscriber should listen for messages + # timeout = 5.0 + + subscriber = pubsub_v1.SubscriberClient() + # The `subscription_path` method creates a fully qualified identifier + # in the form `projects/{project_id}/subscriptions/{subscription_id}` + subscription_path = subscriber.subscription_path(project_id, subscription_id) + + def callback(message: pubsub_v1.subscriber.message.Message) -> None: + print(f"Received {message}.") + + # Use `ack_with_response()` instead of `ack()` to get a future that tracks + # the result of the acknowledge call. When exactly-once delivery is enabled + # on the subscription, the message is guaranteed to not be delivered again + # if the ack future succeeds. + ack_future = message.ack_with_response() + + try: + # Block on result of acknowledge call. + # When `timeout` is not set, result() will block indefinitely, + # unless an exception is encountered first. + ack_future.result(timeout=timeout) + print(f"Ack for message {message.message_id} successful.") + except sub_exceptions.AcknowledgeError as e: + print( + f"Ack for message {message.message_id} failed with error: {e.error_code}" + ) + + streaming_pull_future = subscriber.subscribe(subscription_path, callback=callback) + print(f"Listening for messages on {subscription_path}..\n") + + # Wrap subscriber in a 'with' block to automatically call close() when done. + with subscriber: + try: + # When `timeout` is not set, result() will block indefinitely, + # unless an exception is encountered first. + streaming_pull_future.result(timeout=timeout) + except TimeoutError: + streaming_pull_future.cancel() # Trigger the shutdown. + streaming_pull_future.result() # Block until the shutdown is complete. + # [END pubsub_subscriber_exactly_once] + + def synchronous_pull(project_id: str, subscription_id: str) -> None: """Pulling messages synchronously.""" # [START pubsub_subscriber_sync_pull] @@ -574,6 +691,9 @@ def synchronous_pull(project_id: str, subscription_id: str) -> None: retry=retry.Retry(deadline=300), ) + if len(response.received_messages) == 0: + return + ack_ids = [] for received_message in response.received_messages: print(f"Received: {received_message.message.data}.") @@ -620,6 +740,9 @@ def synchronous_pull_with_lease_management( retry=retry.Retry(deadline=300), ) + if len(response.received_messages) == 0: + return + # Start a process for each message based on its size modulo 10. for message in response.received_messages: process = multiprocessing.Process( @@ -665,7 +788,7 @@ def synchronous_pull_with_lease_management( def listen_for_errors( - project_id: str, subscription_id: str, timeout: float = None + project_id: str, subscription_id: str, timeout: Optional[float] = None ) -> None: """Receives messages and catches errors from a pull subscription.""" # [START pubsub_subscriber_error_listener] @@ -703,7 +826,7 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: def receive_messages_with_delivery_attempts( - project_id: str, subscription_id: str, timeout: float = None + project_id: str, subscription_id: str, timeout: Optional[float] = None ) -> None: # [START pubsub_dead_letter_delivery_attempt] from concurrent.futures import TimeoutError @@ -736,7 +859,7 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: # [END pubsub_dead_letter_delivery_attempt] -if __name__ == "__main__": +if __name__ == "__main__": # noqa parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -780,6 +903,22 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: create_subscription_with_ordering_parser.add_argument("topic_id") create_subscription_with_ordering_parser.add_argument("subscription_id") + create_subscription_with_filtering_parser = subparsers.add_parser( + "create-with-filtering", help=create_subscription_with_filtering.__doc__ + ) + create_subscription_with_filtering_parser.add_argument("topic_id") + create_subscription_with_filtering_parser.add_argument("subscription_id") + create_subscription_with_filtering_parser.add_argument("filter") + + create_subscription_with_exactly_once_delivery_parser = subparsers.add_parser( + "create-with-exactly-once", + help=create_subscription_with_exactly_once_delivery.__doc__, + ) + create_subscription_with_exactly_once_delivery_parser.add_argument("topic_id") + create_subscription_with_exactly_once_delivery_parser.add_argument( + "subscription_id" + ) + delete_parser = subparsers.add_parser("delete", help=delete_subscription.__doc__) delete_parser.add_argument("subscription_id") @@ -837,6 +976,17 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: "timeout", default=None, type=float, nargs="?" ) + receive_messages_with_exactly_once_delivery_enabled_parser = subparsers.add_parser( + "receive-messages-with-exactly-once-delivery-enabled", + help=receive_messages_with_exactly_once_delivery_enabled.__doc__, + ) + receive_messages_with_exactly_once_delivery_enabled_parser.add_argument( + "subscription_id" + ) + receive_messages_with_exactly_once_delivery_enabled_parser.add_argument( + "timeout", default=None, type=float, nargs="?" + ) + synchronous_pull_parser = subparsers.add_parser( "receive-synchronously", help=synchronous_pull.__doc__ ) @@ -883,17 +1033,25 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: ) elif args.command == "create-push": create_push_subscription( - args.project_id, args.topic_id, args.subscription_id, args.endpoint, + args.project_id, args.topic_id, args.subscription_id, args.endpoint ) elif args.command == "create-with-ordering": create_subscription_with_ordering( args.project_id, args.topic_id, args.subscription_id ) + elif args.command == "create-with-filtering": + create_subscription_with_filtering( + args.project_id, args.topic_id, args.subscription_id, args.filter + ) + elif args.command == "create-with-exactly-once": + create_subscription_with_exactly_once_delivery( + args.project_id, args.topic_id, args.subscription_id + ) elif args.command == "delete": delete_subscription(args.project_id, args.subscription_id) elif args.command == "update-push": update_push_subscription( - args.project_id, args.topic_id, args.subscription_id, args.endpoint, + args.project_id, args.topic_id, args.subscription_id, args.endpoint ) elif args.command == "update-dead-letter-policy": update_subscription_with_dead_letter_policy( @@ -919,6 +1077,10 @@ def callback(message: pubsub_v1.subscriber.message.Message) -> None: receive_messages_with_blocking_shutdown( args.project_id, args.subscription_id, args.timeout ) + elif args.command == "receive-messages-with-exactly-once-delivery-enabled": + receive_messages_with_exactly_once_delivery_enabled( + args.project_id, args.subscription_id, args.timeout + ) elif args.command == "receive-synchronously": synchronous_pull(args.project_id, args.subscription_id) elif args.command == "receive-synchronously-with-lease": diff --git a/samples/snippets/subscriber_test.py b/samples/snippets/subscriber_test.py index 574529e80..34a42cac4 100644 --- a/samples/snippets/subscriber_test.py +++ b/samples/snippets/subscriber_test.py @@ -16,7 +16,7 @@ import re import sys import time -from typing import Generator +from typing import Any, Callable, cast, Generator, TypeVar import uuid from _pytest.capture import CaptureFixture @@ -43,6 +43,11 @@ NEW_ENDPOINT = f"https://{PROJECT_ID}.appspot.com/push2" DEFAULT_MAX_DELIVERY_ATTEMPTS = 5 UPDATED_MAX_DELIVERY_ATTEMPTS = 20 +FILTER = 'attributes.author="unknown"' + +C = TypeVar("C", bound=Callable[..., Any]) + +typed_flaky = cast(Callable[[C], C], flaky(max_runs=3, min_passes=1)) @pytest.fixture(scope="module") @@ -126,7 +131,11 @@ def subscription_sync( yield subscription.name - @backoff.on_exception(backoff.expo, Unknown, max_time=300) + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=300), + ) + + @typed_backoff def delete_subscription() -> None: try: subscriber_client.delete_subscription( @@ -197,7 +206,7 @@ def _publish_messages( publisher_client: pubsub_v1.PublisherClient, topic: str, message_num: int = 5, - **attrs: dict, + **attrs: Any, ) -> None: for n in range(message_num): data = f"message {n}".encode("utf-8") @@ -205,8 +214,13 @@ def _publish_messages( publish_future.result() -def test_list_in_topic(subscription_admin: str, capsys: CaptureFixture) -> None: - @backoff.on_exception(backoff.expo, AssertionError, max_time=60) +def test_list_in_topic(subscription_admin: str, capsys: CaptureFixture[str]) -> None: + typed_backoff = cast( + Callable[[C], C], + backoff.on_exception(backoff.expo, AssertionError, max_time=60), + ) + + @typed_backoff def eventually_consistent_test() -> None: subscriber.list_subscriptions_in_topic(PROJECT_ID, TOPIC) out, _ = capsys.readouterr() @@ -215,8 +229,13 @@ def eventually_consistent_test() -> None: eventually_consistent_test() -def test_list_in_project(subscription_admin: str, capsys: CaptureFixture) -> None: - @backoff.on_exception(backoff.expo, AssertionError, max_time=60) +def test_list_in_project(subscription_admin: str, capsys: CaptureFixture[str]) -> None: + typed_backoff = cast( + Callable[[C], C], + backoff.on_exception(backoff.expo, AssertionError, max_time=60), + ) + + @typed_backoff def eventually_consistent_test() -> None: subscriber.list_subscriptions_in_project(PROJECT_ID) out, _ = capsys.readouterr() @@ -228,7 +247,7 @@ def eventually_consistent_test() -> None: def test_create_subscription( subscriber_client: pubsub_v1.SubscriberClient, subscription_admin: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: subscription_path = subscriber_client.subscription_path( PROJECT_ID, SUBSCRIPTION_ADMIN @@ -251,7 +270,7 @@ def test_create_subscription_with_dead_letter_policy( subscriber_client: pubsub_v1.SubscriberClient, subscription_dlq: str, dead_letter_topic: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: try: subscriber_client.delete_subscription( @@ -270,18 +289,23 @@ def test_create_subscription_with_dead_letter_policy( assert f"After {DEFAULT_MAX_DELIVERY_ATTEMPTS} delivery attempts." in out -@flaky(max_runs=3, min_passes=1) +@typed_flaky def test_receive_with_delivery_attempts( publisher_client: pubsub_v1.PublisherClient, topic: str, dead_letter_topic: str, subscription_dlq: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: + typed_backoff = cast( + Callable[[C], C], + backoff.on_exception(backoff.expo, (Unknown, NotFound), max_time=120), + ) + # The dlq subscription raises 404 before it's ready. # We keep retrying up to 10 minutes for mitigating the flakiness. - @backoff.on_exception(backoff.expo, (Unknown, NotFound), max_time=120) + @typed_backoff def run_sample() -> None: _publish_messages(publisher_client, topic) @@ -296,14 +320,19 @@ def run_sample() -> None: assert "With delivery attempts: " in out -@flaky(max_runs=3, min_passes=1) +@typed_flaky def test_update_dead_letter_policy( - subscription_dlq: str, dead_letter_topic: str, capsys: CaptureFixture + subscription_dlq: str, dead_letter_topic: str, capsys: CaptureFixture[str] ) -> None: + typed_backoff = cast( + Callable[[C], C], + backoff.on_exception(backoff.expo, (Unknown, InternalServerError), max_time=60), + ) + # We saw internal server error that suggests to retry. - @backoff.on_exception(backoff.expo, (Unknown, InternalServerError), max_time=60) + @typed_backoff def run_sample() -> None: subscriber.update_subscription_with_dead_letter_policy( PROJECT_ID, @@ -321,9 +350,9 @@ def run_sample() -> None: assert f"max_delivery_attempts: {UPDATED_MAX_DELIVERY_ATTEMPTS}" in out -@flaky(max_runs=3, min_passes=1) +@typed_flaky def test_remove_dead_letter_policy( - subscription_dlq: str, capsys: CaptureFixture + subscription_dlq: str, capsys: CaptureFixture[str] ) -> None: subscription_after_update = subscriber.remove_dead_letter_policy( PROJECT_ID, TOPIC, SUBSCRIPTION_DLQ @@ -337,7 +366,7 @@ def test_remove_dead_letter_policy( def test_create_subscription_with_ordering( subscriber_client: pubsub_v1.SubscriberClient, subscription_admin: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: subscription_path = subscriber_client.subscription_path( PROJECT_ID, SUBSCRIPTION_ADMIN @@ -357,13 +386,67 @@ def test_create_subscription_with_ordering( assert "enable_message_ordering: true" in out +def test_create_subscription_with_filtering( + subscriber_client: pubsub_v1.SubscriberClient, + subscription_admin: str, + capsys: CaptureFixture[str], +) -> None: + subscription_path = subscriber_client.subscription_path( + PROJECT_ID, SUBSCRIPTION_ADMIN + ) + try: + subscriber_client.delete_subscription( + request={"subscription": subscription_path} + ) + except NotFound: + pass + + subscriber.create_subscription_with_filtering( + PROJECT_ID, TOPIC, SUBSCRIPTION_ADMIN, FILTER + ) + + out, _ = capsys.readouterr() + assert "Created subscription with filtering enabled" in out + assert f"{subscription_admin}" in out + assert '"attributes.author=\\"unknown\\""' in out + + +def test_create_subscription_with_exactly_once_delivery( + subscriber_client: pubsub_v1.SubscriberClient, + subscription_admin: str, + capsys: CaptureFixture[str], +) -> None: + subscription_path = subscriber_client.subscription_path( + PROJECT_ID, SUBSCRIPTION_ADMIN + ) + try: + subscriber_client.delete_subscription( + request={"subscription": subscription_path} + ) + except NotFound: + pass + + subscriber.create_subscription_with_exactly_once_delivery( + PROJECT_ID, TOPIC, SUBSCRIPTION_ADMIN + ) + + out, _ = capsys.readouterr() + assert "Created subscription with exactly once delivery enabled" in out + assert f"{subscription_admin}" in out + assert "enable_exactly_once_delivery: true" in out + + def test_create_push_subscription( subscriber_client: pubsub_v1.SubscriberClient, subscription_admin: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=60), + ) + # The scope of `subscription_path` is limited to this function. - @backoff.on_exception(backoff.expo, AssertionError, max_time=60) + @typed_backoff def eventually_consistent_test() -> None: subscription_path = subscriber_client.subscription_path( PROJECT_ID, SUBSCRIPTION_ADMIN @@ -375,7 +458,9 @@ def eventually_consistent_test() -> None: except NotFound: pass - subscriber.create_push_subscription(PROJECT_ID, TOPIC, SUBSCRIPTION_ADMIN, ENDPOINT) + subscriber.create_push_subscription( + PROJECT_ID, TOPIC, SUBSCRIPTION_ADMIN, ENDPOINT + ) out, _ = capsys.readouterr() assert f"{subscription_admin}" in out @@ -384,10 +469,14 @@ def eventually_consistent_test() -> None: def test_update_push_suscription( - subscription_admin: str, - capsys: CaptureFixture, + subscription_admin: str, capsys: CaptureFixture[str], ) -> None: - @backoff.on_exception(backoff.expo, AssertionError, max_time=60) + + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=60), + ) + + @typed_backoff def eventually_consistent_test() -> None: subscriber.update_push_subscription( PROJECT_ID, TOPIC, SUBSCRIPTION_ADMIN, NEW_ENDPOINT @@ -405,7 +494,11 @@ def test_delete_subscription( ) -> None: subscriber.delete_subscription(PROJECT_ID, SUBSCRIPTION_ADMIN) - @backoff.on_exception(backoff.expo, AssertionError, max_time=60) + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=60), + ) + + @typed_backoff def eventually_consistent_test() -> None: with pytest.raises(Exception): subscriber_client.get_subscription( @@ -419,10 +512,14 @@ def test_receive( publisher_client: pubsub_v1.PublisherClient, topic: str, subscription_async: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: - @backoff.on_exception(backoff.expo, Unknown, max_time=60) + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=60), + ) + + @typed_backoff def eventually_consistent_test() -> None: _publish_messages(publisher_client, topic) @@ -440,10 +537,14 @@ def test_receive_with_custom_attributes( publisher_client: pubsub_v1.PublisherClient, topic: str, subscription_async: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: - @backoff.on_exception(backoff.expo, Unknown, max_time=60) + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=60), + ) + + @typed_backoff def eventually_consistent_test() -> None: _publish_messages(publisher_client, topic, origin="python-sample") @@ -464,10 +565,14 @@ def test_receive_with_flow_control( publisher_client: pubsub_v1.PublisherClient, topic: str, subscription_async: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: - @backoff.on_exception(backoff.expo, Unknown, max_time=300) + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=300), + ) + + @typed_backoff def eventually_consistent_test() -> None: _publish_messages(publisher_client, topic) @@ -485,7 +590,7 @@ def test_receive_with_blocking_shutdown( publisher_client: pubsub_v1.PublisherClient, topic: str, subscription_async: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: _received = re.compile(r".*received.*message.*", flags=re.IGNORECASE) @@ -493,7 +598,11 @@ def test_receive_with_blocking_shutdown( _canceled = re.compile(r".*streaming pull future canceled.*", flags=re.IGNORECASE) _shut_down = re.compile(r".*done waiting.*stream shutdown.*", flags=re.IGNORECASE) - @backoff.on_exception(backoff.expo, Unknown, max_time=300) + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=300), + ) + + @typed_backoff def eventually_consistent_test() -> None: _publish_messages(publisher_client, topic, message_num=3) @@ -505,24 +614,14 @@ def eventually_consistent_test() -> None: out_lines = out.splitlines() msg_received_lines = [ - i - for i, line in enumerate(out_lines) - if _received.search(line) - ] - msg_done_lines = [ - i - for i, line in enumerate(out_lines) - if _done.search(line) + i for i, line in enumerate(out_lines) if _received.search(line) ] + msg_done_lines = [i for i, line in enumerate(out_lines) if _done.search(line)] stream_canceled_lines = [ - i - for i, line in enumerate(out_lines) - if _canceled.search(line) + i for i, line in enumerate(out_lines) if _canceled.search(line) ] shutdown_done_waiting_lines = [ - i - for i, line in enumerate(out_lines) - if _shut_down.search(line) + i for i, line in enumerate(out_lines) if _shut_down.search(line) ] try: @@ -550,14 +649,46 @@ def eventually_consistent_test() -> None: eventually_consistent_test() +def test_receive_messages_with_exactly_once_delivery_enabled( + publisher_client: pubsub_v1.PublisherClient, + topic: str, + subscription_async: str, + capsys: CaptureFixture[str], +) -> None: + + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=60), + ) + + @typed_backoff + def eventually_consistent_test() -> None: + _publish_messages(publisher_client, topic) + + subscriber.receive_messages_with_exactly_once_delivery_enabled( + PROJECT_ID, SUBSCRIPTION_ASYNC, 10 + ) + + out, _ = capsys.readouterr() + assert "Listening" in out + assert subscription_async in out + assert "Received" in out + assert "Ack" in out + + eventually_consistent_test() + + def test_listen_for_errors( publisher_client: pubsub_v1.PublisherClient, topic: str, subscription_async: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: - @backoff.on_exception(backoff.expo, Unknown, max_time=60) + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=60), + ) + + @typed_backoff def eventually_consistent_test() -> None: _publish_messages(publisher_client, topic) @@ -574,7 +705,7 @@ def test_receive_synchronously( publisher_client: pubsub_v1.PublisherClient, topic: str, subscription_sync: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: _publish_messages(publisher_client, topic) @@ -586,14 +717,19 @@ def test_receive_synchronously( assert f"{subscription_sync}" in out -@flaky(max_runs=3, min_passes=1) +@typed_flaky def test_receive_synchronously_with_lease( publisher_client: pubsub_v1.PublisherClient, topic: str, subscription_sync: str, - capsys: CaptureFixture, + capsys: CaptureFixture[str], ) -> None: - @backoff.on_exception(backoff.expo, Unknown, max_time=300) + + typed_backoff = cast( + Callable[[C], C], backoff.on_exception(backoff.expo, Unknown, max_time=300), + ) + + @typed_backoff def run_sample() -> None: _publish_messages(publisher_client, topic, message_num=10) # Pausing 10s to allow the subscriber to establish the connection diff --git a/scripts/fixup_pubsub_v1_keywords.py b/scripts/fixup_pubsub_v1_keywords.py index ab7623917..3b6d3d378 100644 --- a/scripts/fixup_pubsub_v1_keywords.py +++ b/scripts/fixup_pubsub_v1_keywords.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ class pubsubCallTransformer(cst.CSTTransformer): 'acknowledge': ('subscription', 'ack_ids', ), 'create_schema': ('parent', 'schema', 'schema_id', ), 'create_snapshot': ('name', 'subscription', 'labels', ), - 'create_subscription': ('name', 'topic', 'push_config', 'ack_deadline_seconds', 'retain_acked_messages', 'message_retention_duration', 'labels', 'enable_message_ordering', 'expiration_policy', 'filter', 'dead_letter_policy', 'retry_policy', 'detached', 'topic_message_retention_duration', ), + 'create_subscription': ('name', 'topic', 'push_config', 'ack_deadline_seconds', 'retain_acked_messages', 'message_retention_duration', 'labels', 'enable_message_ordering', 'expiration_policy', 'filter', 'dead_letter_policy', 'retry_policy', 'detached', 'enable_exactly_once_delivery', 'topic_message_retention_duration', ), 'create_topic': ('name', 'labels', 'message_storage_policy', 'kms_key_name', 'schema_settings', 'satisfies_pzs', 'message_retention_duration', ), 'delete_schema': ('name', ), 'delete_snapshot': ('snapshot', ), diff --git a/setup.cfg b/setup.cfg index a79cb6a60..c3a2b39f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,17 +17,3 @@ # Generated by synthtool. DO NOT EDIT! [bdist_wheel] universal = 1 - -[pytype] -python_version = 3.8 -inputs = - google/cloud/ -exclude = - tests/ - google/pubsub_v1/ # generated code -output = .pytype/ -disable = - # There's some issue with finding some pyi files, thus disabling. - # The issue https://github.com/google/pytype/issues/150 is closed, but the - # error still occurs for some reason. - pyi-error diff --git a/setup.py b/setup.py index e35af7289..e1b259bd6 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ name = "google-cloud-pubsub" description = "Google Cloud Pub/Sub API client library" -version = "2.9.0" +version = "2.10.0" # Should be one of: # 'Development Status :: 3 - Alpha' # 'Development Status :: 4 - Beta' @@ -34,11 +34,11 @@ # Until this issue is closed # https://github.com/googleapis/google-cloud-python/issues/10566 "google-api-core[grpc] >= 1.28.0, <3.0.0dev", - "libcst >= 0.3.10", "proto-plus >= 1.7.1", "grpc-google-iam-v1 >= 0.12.3, < 0.13dev", + "grpcio-status >= 1.16.0", ] -extras = {} +extras = {"libcst": "libcst >= 0.3.10"} # Setup boilerplate below this line. diff --git a/tests/__init__.py b/tests/__init__.py index 4de65971c..e8e1c3845 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 4de65971c..e8e1c3845 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/unit/gapic/__init__.py b/tests/unit/gapic/__init__.py index 4de65971c..e8e1c3845 100644 --- a/tests/unit/gapic/__init__.py +++ b/tests/unit/gapic/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/unit/gapic/pubsub_v1/__init__.py b/tests/unit/gapic/pubsub_v1/__init__.py index 4de65971c..e8e1c3845 100644 --- a/tests/unit/gapic/pubsub_v1/__init__.py +++ b/tests/unit/gapic/pubsub_v1/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/unit/gapic/pubsub_v1/test_publisher.py b/tests/unit/gapic/pubsub_v1/test_publisher.py index 5e2afb7e2..a8c963d5a 100644 --- a/tests/unit/gapic/pubsub_v1/test_publisher.py +++ b/tests/unit/gapic/pubsub_v1/test_publisher.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -240,20 +240,20 @@ def test_publisher_client_client_options(client_class, transport_class, transpor # unsupported value. with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): with pytest.raises(MutualTLSChannelError): - client = client_class() + client = client_class(transport=transport_name) # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): with pytest.raises(ValueError): - client = client_class() + client = client_class(transport=transport_name) # Check the case quota_project_id is provided options = client_options.ClientOptions(quota_project_id="octopus") with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -310,7 +310,7 @@ def test_publisher_client_mtls_env_auto( ) with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) if use_client_cert_env == "false": expected_client_cert_source = None @@ -387,6 +387,83 @@ def test_publisher_client_mtls_env_auto( ) +@pytest.mark.parametrize("client_class", [PublisherClient, PublisherAsyncClient]) +@mock.patch.object( + PublisherClient, "DEFAULT_ENDPOINT", modify_default_endpoint(PublisherClient) +) +@mock.patch.object( + PublisherAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(PublisherAsyncClient), +) +def test_publisher_client_get_mtls_endpoint_and_cert_source(client_class): + mock_client_cert_source = mock.Mock() + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "true". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source == mock_client_cert_source + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "false". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert doesn't exist. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert exists. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=mock_client_cert_source, + ): + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source == mock_client_cert_source + + @pytest.mark.parametrize( "client_class,transport_class,transport_name", [ @@ -405,7 +482,7 @@ def test_publisher_client_client_options_scopes( options = client_options.ClientOptions(scopes=["1", "2"],) with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -419,24 +496,26 @@ def test_publisher_client_client_options_scopes( @pytest.mark.parametrize( - "client_class,transport_class,transport_name", + "client_class,transport_class,transport_name,grpc_helpers", [ - (PublisherClient, transports.PublisherGrpcTransport, "grpc"), + (PublisherClient, transports.PublisherGrpcTransport, "grpc", grpc_helpers), ( PublisherAsyncClient, transports.PublisherGrpcAsyncIOTransport, "grpc_asyncio", + grpc_helpers_async, ), ], ) def test_publisher_client_client_options_credentials_file( - client_class, transport_class, transport_name + client_class, transport_class, transport_name, grpc_helpers ): # Check the case credentials file is provided. options = client_options.ClientOptions(credentials_file="credentials.json") + with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file="credentials.json", @@ -467,7 +546,73 @@ def test_publisher_client_client_options_from_dict(): ) -def test_create_topic(transport: str = "grpc", request_type=pubsub.Topic): +@pytest.mark.parametrize( + "client_class,transport_class,transport_name,grpc_helpers", + [ + (PublisherClient, transports.PublisherGrpcTransport, "grpc", grpc_helpers), + ( + PublisherAsyncClient, + transports.PublisherGrpcAsyncIOTransport, + "grpc_asyncio", + grpc_helpers_async, + ), + ], +) +def test_publisher_client_create_channel_credentials_file( + client_class, transport_class, transport_name, grpc_helpers +): + # Check the case credentials file is provided. + options = client_options.ClientOptions(credentials_file="credentials.json") + + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options, transport=transport_name) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # test that the credentials from file are saved and used as the credentials. + with mock.patch.object( + google.auth, "load_credentials_from_file", autospec=True + ) as load_creds, mock.patch.object( + google.auth, "default", autospec=True + ) as adc, mock.patch.object( + grpc_helpers, "create_channel" + ) as create_channel: + creds = ga_credentials.AnonymousCredentials() + file_creds = ga_credentials.AnonymousCredentials() + load_creds.return_value = (file_creds, None) + adc.return_value = (creds, None) + client = client_class(client_options=options, transport=transport_name) + create_channel.assert_called_with( + "pubsub.googleapis.com:443", + credentials=file_creds, + credentials_file=None, + quota_project_id=None, + default_scopes=( + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/pubsub", + ), + scopes=None, + default_host="pubsub.googleapis.com", + ssl_credentials=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ("grpc.keepalive_time_ms", 30000), + ], + ) + + +@pytest.mark.parametrize("request_type", [pubsub.Topic, dict,]) +def test_create_topic(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -496,10 +641,6 @@ def test_create_topic(transport: str = "grpc", request_type=pubsub.Topic): assert response.satisfies_pzs is True -def test_create_topic_from_dict(): - test_create_topic(request_type=dict) - - def test_create_topic_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. @@ -671,7 +812,8 @@ async def test_create_topic_flattened_error_async(): ) -def test_update_topic(transport: str = "grpc", request_type=pubsub.UpdateTopicRequest): +@pytest.mark.parametrize("request_type", [pubsub.UpdateTopicRequest, dict,]) +def test_update_topic(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -700,10 +842,6 @@ def test_update_topic(transport: str = "grpc", request_type=pubsub.UpdateTopicRe assert response.satisfies_pzs is True -def test_update_topic_from_dict(): - test_update_topic(request_type=dict) - - def test_update_topic_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. @@ -809,7 +947,8 @@ async def test_update_topic_field_headers_async(): assert ("x-goog-request-params", "topic.name=topic.name/value",) in kw["metadata"] -def test_publish(transport: str = "grpc", request_type=pubsub.PublishRequest): +@pytest.mark.parametrize("request_type", [pubsub.PublishRequest, dict,]) +def test_publish(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -834,10 +973,6 @@ def test_publish(transport: str = "grpc", request_type=pubsub.PublishRequest): assert response.message_ids == ["message_ids_value"] -def test_publish_from_dict(): - test_publish(request_type=dict) - - def test_publish_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. @@ -1021,7 +1156,8 @@ async def test_publish_flattened_error_async(): ) -def test_get_topic(transport: str = "grpc", request_type=pubsub.GetTopicRequest): +@pytest.mark.parametrize("request_type", [pubsub.GetTopicRequest, dict,]) +def test_get_topic(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1050,10 +1186,6 @@ def test_get_topic(transport: str = "grpc", request_type=pubsub.GetTopicRequest) assert response.satisfies_pzs is True -def test_get_topic_from_dict(): - test_get_topic(request_type=dict) - - def test_get_topic_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. @@ -1225,7 +1357,8 @@ async def test_get_topic_flattened_error_async(): ) -def test_list_topics(transport: str = "grpc", request_type=pubsub.ListTopicsRequest): +@pytest.mark.parametrize("request_type", [pubsub.ListTopicsRequest, dict,]) +def test_list_topics(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1252,10 +1385,6 @@ def test_list_topics(transport: str = "grpc", request_type=pubsub.ListTopicsRequ assert response.next_page_token == "next_page_token_value" -def test_list_topics_from_dict(): - test_list_topics(request_type=dict) - - def test_list_topics_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. @@ -1425,8 +1554,10 @@ async def test_list_topics_flattened_error_async(): ) -def test_list_topics_pager(): - client = PublisherClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_topics_pager(transport_name: str = "grpc"): + client = PublisherClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client.transport.list_topics), "__call__") as call: @@ -1455,8 +1586,10 @@ def test_list_topics_pager(): assert all(isinstance(i, pubsub.Topic) for i in results) -def test_list_topics_pages(): - client = PublisherClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_topics_pages(transport_name: str = "grpc"): + client = PublisherClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client.transport.list_topics), "__call__") as call: @@ -1531,9 +1664,8 @@ async def test_list_topics_async_pages(): assert page_.raw_page.next_page_token == token -def test_list_topic_subscriptions( - transport: str = "grpc", request_type=pubsub.ListTopicSubscriptionsRequest -): +@pytest.mark.parametrize("request_type", [pubsub.ListTopicSubscriptionsRequest, dict,]) +def test_list_topic_subscriptions(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1564,10 +1696,6 @@ def test_list_topic_subscriptions( assert response.next_page_token == "next_page_token_value" -def test_list_topic_subscriptions_from_dict(): - test_list_topic_subscriptions(request_type=dict) - - def test_list_topic_subscriptions_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. @@ -1753,8 +1881,10 @@ async def test_list_topic_subscriptions_flattened_error_async(): ) -def test_list_topic_subscriptions_pager(): - client = PublisherClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_topic_subscriptions_pager(transport_name: str = "grpc"): + client = PublisherClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( @@ -1788,8 +1918,10 @@ def test_list_topic_subscriptions_pager(): assert all(isinstance(i, str) for i in results) -def test_list_topic_subscriptions_pages(): - client = PublisherClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_topic_subscriptions_pages(transport_name: str = "grpc"): + client = PublisherClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( @@ -1879,9 +2011,8 @@ async def test_list_topic_subscriptions_async_pages(): assert page_.raw_page.next_page_token == token -def test_list_topic_snapshots( - transport: str = "grpc", request_type=pubsub.ListTopicSnapshotsRequest -): +@pytest.mark.parametrize("request_type", [pubsub.ListTopicSnapshotsRequest, dict,]) +def test_list_topic_snapshots(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1911,10 +2042,6 @@ def test_list_topic_snapshots( assert response.next_page_token == "next_page_token_value" -def test_list_topic_snapshots_from_dict(): - test_list_topic_snapshots(request_type=dict) - - def test_list_topic_snapshots_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. @@ -2099,8 +2226,10 @@ async def test_list_topic_snapshots_flattened_error_async(): ) -def test_list_topic_snapshots_pager(): - client = PublisherClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_topic_snapshots_pager(transport_name: str = "grpc"): + client = PublisherClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( @@ -2132,8 +2261,10 @@ def test_list_topic_snapshots_pager(): assert all(isinstance(i, str) for i in results) -def test_list_topic_snapshots_pages(): - client = PublisherClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_topic_snapshots_pages(transport_name: str = "grpc"): + client = PublisherClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( @@ -2217,7 +2348,8 @@ async def test_list_topic_snapshots_async_pages(): assert page_.raw_page.next_page_token == token -def test_delete_topic(transport: str = "grpc", request_type=pubsub.DeleteTopicRequest): +@pytest.mark.parametrize("request_type", [pubsub.DeleteTopicRequest, dict,]) +def test_delete_topic(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -2241,10 +2373,6 @@ def test_delete_topic(transport: str = "grpc", request_type=pubsub.DeleteTopicRe assert response is None -def test_delete_topic_from_dict(): - test_delete_topic(request_type=dict) - - def test_delete_topic_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. @@ -2407,9 +2535,8 @@ async def test_delete_topic_flattened_error_async(): ) -def test_detach_subscription( - transport: str = "grpc", request_type=pubsub.DetachSubscriptionRequest -): +@pytest.mark.parametrize("request_type", [pubsub.DetachSubscriptionRequest, dict,]) +def test_detach_subscription(request_type, transport: str = "grpc"): client = PublisherClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -2435,10 +2562,6 @@ def test_detach_subscription( assert isinstance(response, pubsub.DetachSubscriptionResponse) -def test_detach_subscription_from_dict(): - test_detach_subscription(request_type=dict) - - def test_detach_subscription_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. @@ -2571,6 +2694,23 @@ def test_credentials_transport_error(): transport=transport, ) + # It is an error to provide an api_key and a transport instance. + transport = transports.PublisherGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + options = client_options.ClientOptions() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = PublisherClient(client_options=options, transport=transport,) + + # It is an error to provide an api_key and a credential. + options = mock.Mock() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = PublisherClient( + client_options=options, credentials=ga_credentials.AnonymousCredentials() + ) + # It is an error to provide scopes and a transport instance. transport = transports.PublisherGrpcTransport( credentials=ga_credentials.AnonymousCredentials(), @@ -3115,7 +3255,7 @@ def test_parse_common_location_path(): assert expected == actual -def test_client_withDEFAULT_CLIENT_INFO(): +def test_client_with_default_client_info(): client_info = gapic_v1.client_info.ClientInfo() with mock.patch.object( @@ -3631,3 +3771,33 @@ def test_client_ctx(): with client: pass close.assert_called() + + +@pytest.mark.parametrize( + "client_class,transport_class", + [ + (PublisherClient, transports.PublisherGrpcTransport), + (PublisherAsyncClient, transports.PublisherGrpcAsyncIOTransport), + ], +) +def test_api_key_credentials(client_class, transport_class): + with mock.patch.object( + google.auth._default, "get_api_key_credentials", create=True + ) as get_api_key_credentials: + mock_cred = mock.Mock() + get_api_key_credentials.return_value = mock_cred + options = client_options.ClientOptions() + options.api_key = "api_key" + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=mock_cred, + 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, + always_use_jwt_access=True, + ) diff --git a/tests/unit/gapic/pubsub_v1/test_schema_service.py b/tests/unit/gapic/pubsub_v1/test_schema_service.py index 226c5f818..ed5bca9a1 100644 --- a/tests/unit/gapic/pubsub_v1/test_schema_service.py +++ b/tests/unit/gapic/pubsub_v1/test_schema_service.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -250,20 +250,20 @@ def test_schema_service_client_client_options( # unsupported value. with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): with pytest.raises(MutualTLSChannelError): - client = client_class() + client = client_class(transport=transport_name) # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): with pytest.raises(ValueError): - client = client_class() + client = client_class(transport=transport_name) # Check the case quota_project_id is provided options = client_options.ClientOptions(quota_project_id="octopus") with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -322,7 +322,7 @@ def test_schema_service_client_mtls_env_auto( ) with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) if use_client_cert_env == "false": expected_client_cert_source = None @@ -399,6 +399,87 @@ def test_schema_service_client_mtls_env_auto( ) +@pytest.mark.parametrize( + "client_class", [SchemaServiceClient, SchemaServiceAsyncClient] +) +@mock.patch.object( + SchemaServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(SchemaServiceClient), +) +@mock.patch.object( + SchemaServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(SchemaServiceAsyncClient), +) +def test_schema_service_client_get_mtls_endpoint_and_cert_source(client_class): + mock_client_cert_source = mock.Mock() + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "true". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source == mock_client_cert_source + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "false". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert doesn't exist. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert exists. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=mock_client_cert_source, + ): + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source == mock_client_cert_source + + @pytest.mark.parametrize( "client_class,transport_class,transport_name", [ @@ -417,7 +498,7 @@ def test_schema_service_client_client_options_scopes( options = client_options.ClientOptions(scopes=["1", "2"],) with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -431,24 +512,31 @@ def test_schema_service_client_client_options_scopes( @pytest.mark.parametrize( - "client_class,transport_class,transport_name", + "client_class,transport_class,transport_name,grpc_helpers", [ - (SchemaServiceClient, transports.SchemaServiceGrpcTransport, "grpc"), + ( + SchemaServiceClient, + transports.SchemaServiceGrpcTransport, + "grpc", + grpc_helpers, + ), ( SchemaServiceAsyncClient, transports.SchemaServiceGrpcAsyncIOTransport, "grpc_asyncio", + grpc_helpers_async, ), ], ) def test_schema_service_client_client_options_credentials_file( - client_class, transport_class, transport_name + client_class, transport_class, transport_name, grpc_helpers ): # Check the case credentials file is provided. options = client_options.ClientOptions(credentials_file="credentials.json") + with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file="credentials.json", @@ -481,9 +569,78 @@ def test_schema_service_client_client_options_from_dict(): ) -def test_create_schema( - transport: str = "grpc", request_type=gp_schema.CreateSchemaRequest +@pytest.mark.parametrize( + "client_class,transport_class,transport_name,grpc_helpers", + [ + ( + SchemaServiceClient, + transports.SchemaServiceGrpcTransport, + "grpc", + grpc_helpers, + ), + ( + SchemaServiceAsyncClient, + transports.SchemaServiceGrpcAsyncIOTransport, + "grpc_asyncio", + grpc_helpers_async, + ), + ], +) +def test_schema_service_client_create_channel_credentials_file( + client_class, transport_class, transport_name, grpc_helpers ): + # Check the case credentials file is provided. + options = client_options.ClientOptions(credentials_file="credentials.json") + + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options, transport=transport_name) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # test that the credentials from file are saved and used as the credentials. + with mock.patch.object( + google.auth, "load_credentials_from_file", autospec=True + ) as load_creds, mock.patch.object( + google.auth, "default", autospec=True + ) as adc, mock.patch.object( + grpc_helpers, "create_channel" + ) as create_channel: + creds = ga_credentials.AnonymousCredentials() + file_creds = ga_credentials.AnonymousCredentials() + load_creds.return_value = (file_creds, None) + adc.return_value = (creds, None) + client = client_class(client_options=options, transport=transport_name) + create_channel.assert_called_with( + "pubsub.googleapis.com:443", + credentials=file_creds, + credentials_file=None, + quota_project_id=None, + default_scopes=( + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/pubsub", + ), + scopes=None, + default_host="pubsub.googleapis.com", + ssl_credentials=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ("grpc.keepalive_time_ms", 30000), + ], + ) + + +@pytest.mark.parametrize("request_type", [gp_schema.CreateSchemaRequest, dict,]) +def test_create_schema(request_type, transport: str = "grpc"): client = SchemaServiceClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -514,10 +671,6 @@ def test_create_schema( assert response.definition == "definition_value" -def test_create_schema_from_dict(): - test_create_schema(request_type=dict) - - def test_create_schema_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. @@ -721,7 +874,8 @@ async def test_create_schema_flattened_error_async(): ) -def test_get_schema(transport: str = "grpc", request_type=schema.GetSchemaRequest): +@pytest.mark.parametrize("request_type", [schema.GetSchemaRequest, dict,]) +def test_get_schema(request_type, transport: str = "grpc"): client = SchemaServiceClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -752,10 +906,6 @@ def test_get_schema(transport: str = "grpc", request_type=schema.GetSchemaReques assert response.definition == "definition_value" -def test_get_schema_from_dict(): - test_get_schema(request_type=dict) - - def test_get_schema_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. @@ -933,7 +1083,8 @@ async def test_get_schema_flattened_error_async(): ) -def test_list_schemas(transport: str = "grpc", request_type=schema.ListSchemasRequest): +@pytest.mark.parametrize("request_type", [schema.ListSchemasRequest, dict,]) +def test_list_schemas(request_type, transport: str = "grpc"): client = SchemaServiceClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -960,10 +1111,6 @@ def test_list_schemas(transport: str = "grpc", request_type=schema.ListSchemasRe assert response.next_page_token == "next_page_token_value" -def test_list_schemas_from_dict(): - test_list_schemas(request_type=dict) - - def test_list_schemas_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. @@ -1139,8 +1286,10 @@ async def test_list_schemas_flattened_error_async(): ) -def test_list_schemas_pager(): - client = SchemaServiceClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_schemas_pager(transport_name: str = "grpc"): + client = SchemaServiceClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client.transport.list_schemas), "__call__") as call: @@ -1171,8 +1320,10 @@ def test_list_schemas_pager(): assert all(isinstance(i, schema.Schema) for i in results) -def test_list_schemas_pages(): - client = SchemaServiceClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_schemas_pages(transport_name: str = "grpc"): + client = SchemaServiceClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client.transport.list_schemas), "__call__") as call: @@ -1253,9 +1404,8 @@ async def test_list_schemas_async_pages(): assert page_.raw_page.next_page_token == token -def test_delete_schema( - transport: str = "grpc", request_type=schema.DeleteSchemaRequest -): +@pytest.mark.parametrize("request_type", [schema.DeleteSchemaRequest, dict,]) +def test_delete_schema(request_type, transport: str = "grpc"): client = SchemaServiceClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1279,10 +1429,6 @@ def test_delete_schema( assert response is None -def test_delete_schema_from_dict(): - test_delete_schema(request_type=dict) - - def test_delete_schema_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. @@ -1451,9 +1597,8 @@ async def test_delete_schema_flattened_error_async(): ) -def test_validate_schema( - transport: str = "grpc", request_type=gp_schema.ValidateSchemaRequest -): +@pytest.mark.parametrize("request_type", [gp_schema.ValidateSchemaRequest, dict,]) +def test_validate_schema(request_type, transport: str = "grpc"): client = SchemaServiceClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1477,10 +1622,6 @@ def test_validate_schema( assert isinstance(response, gp_schema.ValidateSchemaResponse) -def test_validate_schema_from_dict(): - test_validate_schema(request_type=dict) - - def test_validate_schema_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. @@ -1669,9 +1810,8 @@ async def test_validate_schema_flattened_error_async(): ) -def test_validate_message( - transport: str = "grpc", request_type=schema.ValidateMessageRequest -): +@pytest.mark.parametrize("request_type", [schema.ValidateMessageRequest, dict,]) +def test_validate_message(request_type, transport: str = "grpc"): client = SchemaServiceClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1695,10 +1835,6 @@ def test_validate_message( assert isinstance(response, schema.ValidateMessageResponse) -def test_validate_message_from_dict(): - test_validate_message(request_type=dict) - - def test_validate_message_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. @@ -1821,6 +1957,23 @@ def test_credentials_transport_error(): transport=transport, ) + # It is an error to provide an api_key and a transport instance. + transport = transports.SchemaServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + options = client_options.ClientOptions() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = SchemaServiceClient(client_options=options, transport=transport,) + + # It is an error to provide an api_key and a credential. + options = mock.Mock() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = SchemaServiceClient( + client_options=options, credentials=ga_credentials.AnonymousCredentials() + ) + # It is an error to provide scopes and a transport instance. transport = transports.SchemaServiceGrpcTransport( credentials=ga_credentials.AnonymousCredentials(), @@ -2335,7 +2488,7 @@ def test_parse_common_location_path(): assert expected == actual -def test_client_withDEFAULT_CLIENT_INFO(): +def test_client_with_default_client_info(): client_info = gapic_v1.client_info.ClientInfo() with mock.patch.object( @@ -2863,3 +3016,33 @@ def test_client_ctx(): with client: pass close.assert_called() + + +@pytest.mark.parametrize( + "client_class,transport_class", + [ + (SchemaServiceClient, transports.SchemaServiceGrpcTransport), + (SchemaServiceAsyncClient, transports.SchemaServiceGrpcAsyncIOTransport), + ], +) +def test_api_key_credentials(client_class, transport_class): + with mock.patch.object( + google.auth._default, "get_api_key_credentials", create=True + ) as get_api_key_credentials: + mock_cred = mock.Mock() + get_api_key_credentials.return_value = mock_cred + options = client_options.ClientOptions() + options.api_key = "api_key" + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=mock_cred, + 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, + always_use_jwt_access=True, + ) diff --git a/tests/unit/gapic/pubsub_v1/test_subscriber.py b/tests/unit/gapic/pubsub_v1/test_subscriber.py index c5ed946a6..c66d92404 100644 --- a/tests/unit/gapic/pubsub_v1/test_subscriber.py +++ b/tests/unit/gapic/pubsub_v1/test_subscriber.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -244,20 +244,20 @@ def test_subscriber_client_client_options( # unsupported value. with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): with pytest.raises(MutualTLSChannelError): - client = client_class() + client = client_class(transport=transport_name) # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): with pytest.raises(ValueError): - client = client_class() + client = client_class(transport=transport_name) # Check the case quota_project_id is provided options = client_options.ClientOptions(quota_project_id="octopus") with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -314,7 +314,7 @@ def test_subscriber_client_mtls_env_auto( ) with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) if use_client_cert_env == "false": expected_client_cert_source = None @@ -391,6 +391,83 @@ def test_subscriber_client_mtls_env_auto( ) +@pytest.mark.parametrize("client_class", [SubscriberClient, SubscriberAsyncClient]) +@mock.patch.object( + SubscriberClient, "DEFAULT_ENDPOINT", modify_default_endpoint(SubscriberClient) +) +@mock.patch.object( + SubscriberAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(SubscriberAsyncClient), +) +def test_subscriber_client_get_mtls_endpoint_and_cert_source(client_class): + mock_client_cert_source = mock.Mock() + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "true". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source == mock_client_cert_source + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "false". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert doesn't exist. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert exists. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=mock_client_cert_source, + ): + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source == mock_client_cert_source + + @pytest.mark.parametrize( "client_class,transport_class,transport_name", [ @@ -409,7 +486,7 @@ def test_subscriber_client_client_options_scopes( options = client_options.ClientOptions(scopes=["1", "2"],) with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -423,24 +500,26 @@ def test_subscriber_client_client_options_scopes( @pytest.mark.parametrize( - "client_class,transport_class,transport_name", + "client_class,transport_class,transport_name,grpc_helpers", [ - (SubscriberClient, transports.SubscriberGrpcTransport, "grpc"), + (SubscriberClient, transports.SubscriberGrpcTransport, "grpc", grpc_helpers), ( SubscriberAsyncClient, transports.SubscriberGrpcAsyncIOTransport, "grpc_asyncio", + grpc_helpers_async, ), ], ) def test_subscriber_client_client_options_credentials_file( - client_class, transport_class, transport_name + client_class, transport_class, transport_name, grpc_helpers ): # Check the case credentials file is provided. options = client_options.ClientOptions(credentials_file="credentials.json") + with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(transport=transport_name, client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file="credentials.json", @@ -471,7 +550,73 @@ def test_subscriber_client_client_options_from_dict(): ) -def test_create_subscription(transport: str = "grpc", request_type=pubsub.Subscription): +@pytest.mark.parametrize( + "client_class,transport_class,transport_name,grpc_helpers", + [ + (SubscriberClient, transports.SubscriberGrpcTransport, "grpc", grpc_helpers), + ( + SubscriberAsyncClient, + transports.SubscriberGrpcAsyncIOTransport, + "grpc_asyncio", + grpc_helpers_async, + ), + ], +) +def test_subscriber_client_create_channel_credentials_file( + client_class, transport_class, transport_name, grpc_helpers +): + # Check the case credentials file is provided. + options = client_options.ClientOptions(credentials_file="credentials.json") + + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options, transport=transport_name) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # test that the credentials from file are saved and used as the credentials. + with mock.patch.object( + google.auth, "load_credentials_from_file", autospec=True + ) as load_creds, mock.patch.object( + google.auth, "default", autospec=True + ) as adc, mock.patch.object( + grpc_helpers, "create_channel" + ) as create_channel: + creds = ga_credentials.AnonymousCredentials() + file_creds = ga_credentials.AnonymousCredentials() + load_creds.return_value = (file_creds, None) + adc.return_value = (creds, None) + client = client_class(client_options=options, transport=transport_name) + create_channel.assert_called_with( + "pubsub.googleapis.com:443", + credentials=file_creds, + credentials_file=None, + quota_project_id=None, + default_scopes=( + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/pubsub", + ), + scopes=None, + default_host="pubsub.googleapis.com", + ssl_credentials=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ("grpc.keepalive_time_ms", 30000), + ], + ) + + +@pytest.mark.parametrize("request_type", [pubsub.Subscription, dict,]) +def test_create_subscription(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -493,6 +638,7 @@ def test_create_subscription(transport: str = "grpc", request_type=pubsub.Subscr enable_message_ordering=True, filter="filter_value", detached=True, + enable_exactly_once_delivery=True, ) response = client.create_subscription(request) @@ -510,10 +656,7 @@ def test_create_subscription(transport: str = "grpc", request_type=pubsub.Subscr assert response.enable_message_ordering is True assert response.filter == "filter_value" assert response.detached is True - - -def test_create_subscription_from_dict(): - test_create_subscription(request_type=dict) + assert response.enable_exactly_once_delivery is True def test_create_subscription_empty_call(): @@ -559,6 +702,7 @@ async def test_create_subscription_async( enable_message_ordering=True, filter="filter_value", detached=True, + enable_exactly_once_delivery=True, ) ) response = await client.create_subscription(request) @@ -577,6 +721,7 @@ async def test_create_subscription_async( assert response.enable_message_ordering is True assert response.filter == "filter_value" assert response.detached is True + assert response.enable_exactly_once_delivery is True @pytest.mark.asyncio @@ -743,9 +888,8 @@ async def test_create_subscription_flattened_error_async(): ) -def test_get_subscription( - transport: str = "grpc", request_type=pubsub.GetSubscriptionRequest -): +@pytest.mark.parametrize("request_type", [pubsub.GetSubscriptionRequest, dict,]) +def test_get_subscription(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -765,6 +909,7 @@ def test_get_subscription( enable_message_ordering=True, filter="filter_value", detached=True, + enable_exactly_once_delivery=True, ) response = client.get_subscription(request) @@ -782,10 +927,7 @@ def test_get_subscription( assert response.enable_message_ordering is True assert response.filter == "filter_value" assert response.detached is True - - -def test_get_subscription_from_dict(): - test_get_subscription(request_type=dict) + assert response.enable_exactly_once_delivery is True def test_get_subscription_empty_call(): @@ -827,6 +969,7 @@ async def test_get_subscription_async( enable_message_ordering=True, filter="filter_value", detached=True, + enable_exactly_once_delivery=True, ) ) response = await client.get_subscription(request) @@ -845,6 +988,7 @@ async def test_get_subscription_async( assert response.enable_message_ordering is True assert response.filter == "filter_value" assert response.detached is True + assert response.enable_exactly_once_delivery is True @pytest.mark.asyncio @@ -971,9 +1115,8 @@ async def test_get_subscription_flattened_error_async(): ) -def test_update_subscription( - transport: str = "grpc", request_type=pubsub.UpdateSubscriptionRequest -): +@pytest.mark.parametrize("request_type", [pubsub.UpdateSubscriptionRequest, dict,]) +def test_update_subscription(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -995,6 +1138,7 @@ def test_update_subscription( enable_message_ordering=True, filter="filter_value", detached=True, + enable_exactly_once_delivery=True, ) response = client.update_subscription(request) @@ -1012,10 +1156,7 @@ def test_update_subscription( assert response.enable_message_ordering is True assert response.filter == "filter_value" assert response.detached is True - - -def test_update_subscription_from_dict(): - test_update_subscription(request_type=dict) + assert response.enable_exactly_once_delivery is True def test_update_subscription_empty_call(): @@ -1061,6 +1202,7 @@ async def test_update_subscription_async( enable_message_ordering=True, filter="filter_value", detached=True, + enable_exactly_once_delivery=True, ) ) response = await client.update_subscription(request) @@ -1079,6 +1221,7 @@ async def test_update_subscription_async( assert response.enable_message_ordering is True assert response.filter == "filter_value" assert response.detached is True + assert response.enable_exactly_once_delivery is True @pytest.mark.asyncio @@ -1145,9 +1288,8 @@ async def test_update_subscription_field_headers_async(): ) in kw["metadata"] -def test_list_subscriptions( - transport: str = "grpc", request_type=pubsub.ListSubscriptionsRequest -): +@pytest.mark.parametrize("request_type", [pubsub.ListSubscriptionsRequest, dict,]) +def test_list_subscriptions(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1176,10 +1318,6 @@ def test_list_subscriptions( assert response.next_page_token == "next_page_token_value" -def test_list_subscriptions_from_dict(): - test_list_subscriptions(request_type=dict) - - def test_list_subscriptions_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. @@ -1361,8 +1499,10 @@ async def test_list_subscriptions_flattened_error_async(): ) -def test_list_subscriptions_pager(): - client = SubscriberClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_subscriptions_pager(transport_name: str = "grpc"): + client = SubscriberClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( @@ -1401,8 +1541,10 @@ def test_list_subscriptions_pager(): assert all(isinstance(i, pubsub.Subscription) for i in results) -def test_list_subscriptions_pages(): - client = SubscriberClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_subscriptions_pages(transport_name: str = "grpc"): + client = SubscriberClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( @@ -1507,9 +1649,8 @@ async def test_list_subscriptions_async_pages(): assert page_.raw_page.next_page_token == token -def test_delete_subscription( - transport: str = "grpc", request_type=pubsub.DeleteSubscriptionRequest -): +@pytest.mark.parametrize("request_type", [pubsub.DeleteSubscriptionRequest, dict,]) +def test_delete_subscription(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1535,10 +1676,6 @@ def test_delete_subscription( assert response is None -def test_delete_subscription_from_dict(): - test_delete_subscription(request_type=dict) - - def test_delete_subscription_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. @@ -1717,9 +1854,8 @@ async def test_delete_subscription_flattened_error_async(): ) -def test_modify_ack_deadline( - transport: str = "grpc", request_type=pubsub.ModifyAckDeadlineRequest -): +@pytest.mark.parametrize("request_type", [pubsub.ModifyAckDeadlineRequest, dict,]) +def test_modify_ack_deadline(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1745,10 +1881,6 @@ def test_modify_ack_deadline( assert response is None -def test_modify_ack_deadline_from_dict(): - test_modify_ack_deadline(request_type=dict) - - def test_modify_ack_deadline_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. @@ -1953,7 +2085,8 @@ async def test_modify_ack_deadline_flattened_error_async(): ) -def test_acknowledge(transport: str = "grpc", request_type=pubsub.AcknowledgeRequest): +@pytest.mark.parametrize("request_type", [pubsub.AcknowledgeRequest, dict,]) +def test_acknowledge(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -1977,10 +2110,6 @@ def test_acknowledge(transport: str = "grpc", request_type=pubsub.AcknowledgeReq assert response is None -def test_acknowledge_from_dict(): - test_acknowledge(request_type=dict) - - def test_acknowledge_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. @@ -2161,7 +2290,8 @@ async def test_acknowledge_flattened_error_async(): ) -def test_pull(transport: str = "grpc", request_type=pubsub.PullRequest): +@pytest.mark.parametrize("request_type", [pubsub.PullRequest, dict,]) +def test_pull(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -2185,10 +2315,6 @@ def test_pull(transport: str = "grpc", request_type=pubsub.PullRequest): assert isinstance(response, pubsub.PullResponse) -def test_pull_from_dict(): - test_pull(request_type=dict) - - def test_pull_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. @@ -2385,9 +2511,8 @@ async def test_pull_flattened_error_async(): ) -def test_streaming_pull( - transport: str = "grpc", request_type=pubsub.StreamingPullRequest -): +@pytest.mark.parametrize("request_type", [pubsub.StreamingPullRequest, dict,]) +def test_streaming_pull(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -2413,10 +2538,6 @@ def test_streaming_pull( assert isinstance(message, pubsub.StreamingPullResponse) -def test_streaming_pull_from_dict(): - test_streaming_pull(request_type=dict) - - @pytest.mark.asyncio async def test_streaming_pull_async( transport: str = "grpc_asyncio", request_type=pubsub.StreamingPullRequest @@ -2454,9 +2575,8 @@ async def test_streaming_pull_async_from_dict(): await test_streaming_pull_async(request_type=dict) -def test_modify_push_config( - transport: str = "grpc", request_type=pubsub.ModifyPushConfigRequest -): +@pytest.mark.parametrize("request_type", [pubsub.ModifyPushConfigRequest, dict,]) +def test_modify_push_config(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -2482,10 +2602,6 @@ def test_modify_push_config( assert response is None -def test_modify_push_config_from_dict(): - test_modify_push_config(request_type=dict) - - def test_modify_push_config_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. @@ -2680,7 +2796,8 @@ async def test_modify_push_config_flattened_error_async(): ) -def test_get_snapshot(transport: str = "grpc", request_type=pubsub.GetSnapshotRequest): +@pytest.mark.parametrize("request_type", [pubsub.GetSnapshotRequest, dict,]) +def test_get_snapshot(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -2706,10 +2823,6 @@ def test_get_snapshot(transport: str = "grpc", request_type=pubsub.GetSnapshotRe assert response.topic == "topic_value" -def test_get_snapshot_from_dict(): - test_get_snapshot(request_type=dict) - - def test_get_snapshot_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. @@ -2876,9 +2989,8 @@ async def test_get_snapshot_flattened_error_async(): ) -def test_list_snapshots( - transport: str = "grpc", request_type=pubsub.ListSnapshotsRequest -): +@pytest.mark.parametrize("request_type", [pubsub.ListSnapshotsRequest, dict,]) +def test_list_snapshots(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -2905,10 +3017,6 @@ def test_list_snapshots( assert response.next_page_token == "next_page_token_value" -def test_list_snapshots_from_dict(): - test_list_snapshots(request_type=dict) - - def test_list_snapshots_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. @@ -3078,8 +3186,10 @@ async def test_list_snapshots_flattened_error_async(): ) -def test_list_snapshots_pager(): - client = SubscriberClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_snapshots_pager(transport_name: str = "grpc"): + client = SubscriberClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client.transport.list_snapshots), "__call__") as call: @@ -3112,8 +3222,10 @@ def test_list_snapshots_pager(): assert all(isinstance(i, pubsub.Snapshot) for i in results) -def test_list_snapshots_pages(): - client = SubscriberClient(credentials=ga_credentials.AnonymousCredentials,) +def test_list_snapshots_pages(transport_name: str = "grpc"): + client = SubscriberClient( + credentials=ga_credentials.AnonymousCredentials, transport=transport_name, + ) # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object(type(client.transport.list_snapshots), "__call__") as call: @@ -3200,9 +3312,8 @@ async def test_list_snapshots_async_pages(): assert page_.raw_page.next_page_token == token -def test_create_snapshot( - transport: str = "grpc", request_type=pubsub.CreateSnapshotRequest -): +@pytest.mark.parametrize("request_type", [pubsub.CreateSnapshotRequest, dict,]) +def test_create_snapshot(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -3228,10 +3339,6 @@ def test_create_snapshot( assert response.topic == "topic_value" -def test_create_snapshot_from_dict(): - test_create_snapshot(request_type=dict) - - def test_create_snapshot_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. @@ -3412,9 +3519,8 @@ async def test_create_snapshot_flattened_error_async(): ) -def test_update_snapshot( - transport: str = "grpc", request_type=pubsub.UpdateSnapshotRequest -): +@pytest.mark.parametrize("request_type", [pubsub.UpdateSnapshotRequest, dict,]) +def test_update_snapshot(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -3440,10 +3546,6 @@ def test_update_snapshot( assert response.topic == "topic_value" -def test_update_snapshot_from_dict(): - test_update_snapshot(request_type=dict) - - def test_update_snapshot_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. @@ -3548,9 +3650,8 @@ async def test_update_snapshot_field_headers_async(): ] -def test_delete_snapshot( - transport: str = "grpc", request_type=pubsub.DeleteSnapshotRequest -): +@pytest.mark.parametrize("request_type", [pubsub.DeleteSnapshotRequest, dict,]) +def test_delete_snapshot(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -3574,10 +3675,6 @@ def test_delete_snapshot( assert response is None -def test_delete_snapshot_from_dict(): - test_delete_snapshot(request_type=dict) - - def test_delete_snapshot_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. @@ -3740,7 +3837,8 @@ async def test_delete_snapshot_flattened_error_async(): ) -def test_seek(transport: str = "grpc", request_type=pubsub.SeekRequest): +@pytest.mark.parametrize("request_type", [pubsub.SeekRequest, dict,]) +def test_seek(request_type, transport: str = "grpc"): client = SubscriberClient( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) @@ -3764,10 +3862,6 @@ def test_seek(transport: str = "grpc", request_type=pubsub.SeekRequest): assert isinstance(response, pubsub.SeekResponse) -def test_seek_from_dict(): - test_seek(request_type=dict) - - def test_seek_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. @@ -3888,6 +3982,23 @@ def test_credentials_transport_error(): transport=transport, ) + # It is an error to provide an api_key and a transport instance. + transport = transports.SubscriberGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + options = client_options.ClientOptions() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = SubscriberClient(client_options=options, transport=transport,) + + # It is an error to provide an api_key and a credential. + options = mock.Mock() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = SubscriberClient( + client_options=options, credentials=ga_credentials.AnonymousCredentials() + ) + # It is an error to provide scopes and a transport instance. transport = transports.SubscriberGrpcTransport( credentials=ga_credentials.AnonymousCredentials(), @@ -4439,7 +4550,7 @@ def test_parse_common_location_path(): assert expected == actual -def test_client_withDEFAULT_CLIENT_INFO(): +def test_client_with_default_client_info(): client_info = gapic_v1.client_info.ClientInfo() with mock.patch.object( @@ -4955,3 +5066,33 @@ def test_client_ctx(): with client: pass close.assert_called() + + +@pytest.mark.parametrize( + "client_class,transport_class", + [ + (SubscriberClient, transports.SubscriberGrpcTransport), + (SubscriberAsyncClient, transports.SubscriberGrpcAsyncIOTransport), + ], +) +def test_api_key_credentials(client_class, transport_class): + with mock.patch.object( + google.auth._default, "get_api_key_credentials", create=True + ) as get_api_key_credentials: + mock_cred = mock.Mock() + get_api_key_credentials.return_value = mock_cred + options = client_options.ClientOptions() + options.api_key = "api_key" + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=mock_cred, + 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, + always_use_jwt_access=True, + ) diff --git a/tests/unit/pubsub_v1/publisher/batch/test_thread.py b/tests/unit/pubsub_v1/publisher/batch/test_thread.py index abf5ec76f..b15128489 100644 --- a/tests/unit/pubsub_v1/publisher/batch/test_thread.py +++ b/tests/unit/pubsub_v1/publisher/batch/test_thread.py @@ -128,7 +128,7 @@ def test_blocking__commit(): # Set up the underlying API publish method to return a PublishResponse. publish_response = gapic_types.PublishResponse(message_ids=["a", "b"]) patch = mock.patch.object( - type(batch.client.api), "publish", return_value=publish_response + type(batch.client), "_gapic_publish", return_value=publish_response ) with patch as publish: batch._commit() @@ -160,7 +160,7 @@ def test_blocking__commit_custom_retry(): # Set up the underlying API publish method to return a PublishResponse. publish_response = gapic_types.PublishResponse(message_ids=["a"]) patch = mock.patch.object( - type(batch.client.api), "publish", return_value=publish_response + type(batch.client), "_gapic_publish", return_value=publish_response ) with patch as publish: batch._commit() @@ -182,7 +182,7 @@ def test_blocking__commit_custom_timeout(): # Set up the underlying API publish method to return a PublishResponse. publish_response = gapic_types.PublishResponse(message_ids=["a"]) patch = mock.patch.object( - type(batch.client.api), "publish", return_value=publish_response + type(batch.client), "_gapic_publish", return_value=publish_response ) with patch as publish: batch._commit() @@ -208,7 +208,7 @@ def api_publish_delay(topic="", messages=(), retry=None, timeout=None): return gapic_types.PublishResponse(message_ids=message_ids) api_publish_patch = mock.patch.object( - type(batch.client.api), "publish", side_effect=api_publish_delay + type(batch.client), "_gapic_publish", side_effect=api_publish_delay ) with api_publish_patch: @@ -252,7 +252,7 @@ def test_blocking__commit_already_started(_LOGGER): def test_blocking__commit_no_messages(): batch = create_batch() - with mock.patch.object(type(batch.client.api), "publish") as publish: + with mock.patch.object(type(batch.client), "_gapic_publish") as publish: batch._commit() assert publish.call_count == 0 @@ -268,7 +268,7 @@ def test_blocking__commit_wrong_messageid_length(): # Set up a PublishResponse that only returns one message ID. publish_response = gapic_types.PublishResponse(message_ids=["a"]) patch = mock.patch.object( - type(batch.client.api), "publish", return_value=publish_response + type(batch.client), "_gapic_publish", return_value=publish_response ) with patch: @@ -288,7 +288,7 @@ def test_block__commmit_api_error(): # Make the API throw an error when publishing. error = google.api_core.exceptions.InternalServerError("uh oh") - patch = mock.patch.object(type(batch.client.api), "publish", side_effect=error) + patch = mock.patch.object(type(batch.client), "_gapic_publish", side_effect=error) with patch: batch._commit() @@ -307,7 +307,7 @@ def test_block__commmit_retry_error(): # Make the API throw an error when publishing. error = google.api_core.exceptions.RetryError("uh oh", None) - patch = mock.patch.object(type(batch.client.api), "publish", side_effect=error) + patch = mock.patch.object(type(batch.client), "_gapic_publish", side_effect=error) with patch: batch._commit() @@ -536,7 +536,7 @@ def test_batch_done_callback_called_on_success(): publish_response = gapic_types.PublishResponse(message_ids=["a"]) with mock.patch.object( - type(batch.client.api), "publish", return_value=publish_response + type(batch.client), "_gapic_publish", return_value=publish_response ): batch._commit() @@ -559,8 +559,8 @@ def test_batch_done_callback_called_on_publish_failure(): error = google.api_core.exceptions.InternalServerError("uh oh") with mock.patch.object( - type(batch.client.api), - "publish", + type(batch.client), + "_gapic_publish", return_value=publish_response, side_effect=error, ): @@ -582,7 +582,7 @@ def test_batch_done_callback_called_on_publish_response_invalid(): publish_response = gapic_types.PublishResponse(message_ids=[]) with mock.patch.object( - type(batch.client.api), "publish", return_value=publish_response + type(batch.client), "_gapic_publish", return_value=publish_response ): batch._commit() diff --git a/tests/unit/pubsub_v1/publisher/test_publisher_client.py b/tests/unit/pubsub_v1/publisher/test_publisher_client.py index 161f9e33b..20d5b328c 100644 --- a/tests/unit/pubsub_v1/publisher/test_publisher_client.py +++ b/tests/unit/pubsub_v1/publisher/test_publisher_client.py @@ -22,6 +22,7 @@ import mock import pytest import time +import warnings from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -50,12 +51,42 @@ def _assert_retries_equal(retry, retry2): assert inspect.getclosurevars(pred) == inspect.getclosurevars(pred2) +def test_api_property_deprecated(creds): + client = publisher.Client(credentials=creds) + + with warnings.catch_warnings(record=True) as warned: + client.api + + assert len(warned) == 1 + assert issubclass(warned[0].category, DeprecationWarning) + warning_msg = str(warned[0].message) + assert "client.api" in warning_msg + + +def test_api_property_proxy_to_generated_client(creds): + client = publisher.Client(credentials=creds) + + with warnings.catch_warnings(record=True): + api_object = client.api + + # Not a perfect check, but we are satisficed if the returned API object indeed + # contains all methods of the generated class. + superclass_attrs = (attr for attr in dir(type(client).__mro__[1])) + assert all( + hasattr(api_object, attr) + for attr in superclass_attrs + if callable(getattr(client, attr)) + ) + + # The resume_publish() method only exists on the hand-written wrapper class. + assert hasattr(client, "resume_publish") + assert not hasattr(api_object, "resume_publish") + + def test_init(creds): client = publisher.Client(credentials=creds) - # A plain client should have an `api` (the underlying GAPIC) and a - # batch settings object, which should have the defaults. - assert isinstance(client.api, publisher_client.PublisherClient) + # A plain client should have a batch settings object containing the defaults. assert client.batch_settings.max_bytes == 1 * 1000 * 1000 assert client.batch_settings.max_latency == 0.01 assert client.batch_settings.max_messages == 100 @@ -67,7 +98,7 @@ def test_init_default_client_info(creds): installed_version = publisher.client.__version__ expected_client_info = f"gccl/{installed_version}" - for wrapped_method in client.api.transport._wrapped_methods.values(): + for wrapped_method in client.transport._wrapped_methods.values(): user_agent = next( ( header_value @@ -84,10 +115,10 @@ def test_init_w_custom_transport(creds): transport = PublisherGrpcTransport(credentials=creds) client = publisher.Client(transport=transport) - # A plain client should have an `api` (the underlying GAPIC) and a - # batch settings object, which should have the defaults. - assert isinstance(client.api, publisher_client.PublisherClient) - assert client.api._transport is transport + # A plain client should have a transport and a batch settings object, which should + # contain the defaults. + assert isinstance(client, publisher_client.PublisherClient) + assert client._transport is transport assert client.batch_settings.max_bytes == 1 * 1000 * 1000 assert client.batch_settings.max_latency == 0.01 assert client.batch_settings.max_messages == 100 @@ -97,8 +128,7 @@ def test_init_w_api_endpoint(creds): client_options = {"api_endpoint": "testendpoint.google.com"} client = publisher.Client(client_options=client_options, credentials=creds) - assert isinstance(client.api, publisher_client.PublisherClient) - assert (client.api._transport.grpc_channel._channel.target()).decode( + assert (client._transport.grpc_channel._channel.target()).decode( "utf-8" ) == "testendpoint.google.com:443" @@ -106,8 +136,7 @@ def test_init_w_api_endpoint(creds): def test_init_w_empty_client_options(creds): client = publisher.Client(client_options={}, credentials=creds) - assert isinstance(client.api, publisher_client.PublisherClient) - assert (client.api._transport.grpc_channel._channel.target()).decode( + assert (client._transport.grpc_channel._channel.target()).decode( "utf-8" ) == publisher_client.PublisherClient.SERVICE_ADDRESS @@ -129,12 +158,12 @@ def init(self, *args, **kwargs): "credentials_file": "file.json", } ) - client_options = client.api.kwargs["client_options"] + client_options = client.kwargs["client_options"] assert client_options.get("quota_project_id") == "42" assert client_options.get("scopes") == [] assert client_options.get("credentials_file") == "file.json" assert client.target == "testendpoint.google.com" - assert client.api.transport._ssl_channel_credentials == mock_ssl_creds + assert client.transport._ssl_channel_credentials == mock_ssl_creds def test_init_emulator(monkeypatch): @@ -147,7 +176,7 @@ def test_init_emulator(monkeypatch): # # Sadly, there seems to be no good way to do this without poking at # the private API of gRPC. - channel = client.api._transport.publish._channel + channel = client._transport.publish._channel assert channel.target().decode("utf8") == "/foo/bar:123" @@ -418,7 +447,7 @@ def test_gapic_instance_method(creds): transport_mock._wrapped_methods = { transport_mock.create_topic: fake_create_topic_rpc } - patcher = mock.patch.object(client.api, "_transport", new=transport_mock) + patcher = mock.patch.object(client, "_transport", new=transport_mock) topic = gapic_types.Topic(name="projects/foo/topics/bar") diff --git a/tests/unit/pubsub_v1/subscriber/test_dispatcher.py b/tests/unit/pubsub_v1/subscriber/test_dispatcher.py index 84e04df1b..bbc6170e2 100644 --- a/tests/unit/pubsub_v1/subscriber/test_dispatcher.py +++ b/tests/unit/pubsub_v1/subscriber/test_dispatcher.py @@ -20,7 +20,7 @@ from google.cloud.pubsub_v1.subscriber._protocol import helper_threads from google.cloud.pubsub_v1.subscriber._protocol import requests from google.cloud.pubsub_v1.subscriber._protocol import streaming_pull_manager -from google.pubsub_v1 import types as gapic_types +from google.cloud.pubsub_v1.subscriber import futures import mock import pytest @@ -29,11 +29,11 @@ @pytest.mark.parametrize( "item,method_name", [ - (requests.AckRequest("0", 0, 0, ""), "ack"), + (requests.AckRequest("0", 0, 0, "", None), "ack"), (requests.DropRequest("0", 0, ""), "drop"), (requests.LeaseRequest("0", 0, ""), "lease"), - (requests.ModAckRequest("0", 0), "modify_ack_deadline"), - (requests.NackRequest("0", 0, ""), "nack"), + (requests.ModAckRequest("0", 0, None), "modify_ack_deadline"), + (requests.NackRequest("0", 0, "", None), "nack"), ], ) def test_dispatch_callback_active_manager(item, method_name): @@ -53,11 +53,11 @@ def test_dispatch_callback_active_manager(item, method_name): @pytest.mark.parametrize( "item,method_name", [ - (requests.AckRequest("0", 0, 0, ""), "ack"), + (requests.AckRequest("0", 0, 0, "", None), "ack"), (requests.DropRequest("0", 0, ""), "drop"), (requests.LeaseRequest("0", 0, ""), "lease"), - (requests.ModAckRequest("0", 0), "modify_ack_deadline"), - (requests.NackRequest("0", 0, ""), "nack"), + (requests.ModAckRequest("0", 0, None), "modify_ack_deadline"), + (requests.NackRequest("0", 0, "", None), "nack"), ], ) def test_dispatch_callback_inactive_manager(item, method_name): @@ -75,6 +75,17 @@ def test_dispatch_callback_inactive_manager(item, method_name): method.assert_called_once_with([item]) +def test_unknown_request_type(): + manager = mock.create_autospec( + streaming_pull_manager.StreamingPullManager, instance=True + ) + dispatcher_ = dispatcher.Dispatcher(manager, mock.sentinel.queue) + + items = ["a random string, not a known request type"] + manager.send_unary_ack.return_value = (items, []) + dispatcher_.dispatch_callback(items) + + def test_ack(): manager = mock.create_autospec( streaming_pull_manager.StreamingPullManager, instance=True @@ -83,13 +94,18 @@ def test_ack(): items = [ requests.AckRequest( - ack_id="ack_id_string", byte_size=0, time_to_ack=20, ordering_key="" + ack_id="ack_id_string", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=None, ) ] + manager.send_unary_ack.return_value = (items, []) dispatcher_.ack(items) - manager.send.assert_called_once_with( - gapic_types.StreamingPullRequest(ack_ids=["ack_id_string"]) + manager.send_unary_ack.assert_called_once_with( + ack_ids=["ack_id_string"], ack_reqs_dict={"ack_id_string": items[0]} ) manager.leaser.remove.assert_called_once_with(items) @@ -105,13 +121,18 @@ def test_ack_no_time(): items = [ requests.AckRequest( - ack_id="ack_id_string", byte_size=0, time_to_ack=None, ordering_key="" + ack_id="ack_id_string", + byte_size=0, + time_to_ack=None, + ordering_key="", + future=None, ) ] + manager.send_unary_ack.return_value = (items, []) dispatcher_.ack(items) - manager.send.assert_called_once_with( - gapic_types.StreamingPullRequest(ack_ids=["ack_id_string"]) + manager.send_unary_ack.assert_called_once_with( + ack_ids=["ack_id_string"], ack_reqs_dict={"ack_id_string": items[0]} ) manager.ack_histogram.add.assert_not_called() @@ -126,27 +147,152 @@ def test_ack_splitting_large_payload(): items = [ # use realistic lengths for ACK IDs (max 176 bytes) requests.AckRequest( - ack_id=str(i).zfill(176), byte_size=0, time_to_ack=20, ordering_key="" + ack_id=str(i).zfill(176), + byte_size=0, + time_to_ack=20, + ordering_key="", + future=None, ) for i in range(5001) ] + manager.send_unary_ack.return_value = (items, []) dispatcher_.ack(items) - calls = manager.send.call_args_list + calls = manager.send_unary_ack.call_args_list assert len(calls) == 3 all_ack_ids = {item.ack_id for item in items} sent_ack_ids = collections.Counter() for call in calls: - message = call.args[0] - assert message._pb.ByteSize() <= 524288 # server-side limit (2**19) - sent_ack_ids.update(message.ack_ids) + ack_ids = call[1]["ack_ids"] + assert len(ack_ids) <= dispatcher._ACK_IDS_BATCH_SIZE + sent_ack_ids.update(ack_ids) assert set(sent_ack_ids) == all_ack_ids # all messages should have been ACK-ed assert sent_ack_ids.most_common(1)[0][1] == 1 # each message ACK-ed exactly once +def test_retry_acks_in_new_thread(): + manager = mock.create_autospec( + streaming_pull_manager.StreamingPullManager, instance=True + ) + dispatcher_ = dispatcher.Dispatcher(manager, mock.sentinel.queue) + + f = futures.Future() + items = [ + requests.AckRequest( + ack_id="ack_id_string", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=f, + ) + ] + # failure triggers creation of new retry thread + manager.send_unary_ack.side_effect = [([], items)] + with mock.patch("time.sleep", return_value=None): + with mock.patch.object(threading, "Thread", autospec=True) as Thread: + dispatcher_.ack(items) + + assert len(Thread.mock_calls) == 2 + ctor_call = Thread.mock_calls[0] + assert ctor_call.kwargs["name"] == "Thread-RetryAcks" + assert ctor_call.kwargs["target"].args[0] == items + assert ctor_call.kwargs["daemon"] + + +def test_retry_acks(): + manager = mock.create_autospec( + streaming_pull_manager.StreamingPullManager, instance=True + ) + dispatcher_ = dispatcher.Dispatcher(manager, mock.sentinel.queue) + + f = futures.Future() + items = [ + requests.AckRequest( + ack_id="ack_id_string", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=f, + ) + ] + # first and second `send_unary_ack` calls fail, third one succeeds + manager.send_unary_ack.side_effect = [([], items), ([], items), (items, [])] + with mock.patch("time.sleep", return_value=None): + dispatcher_._retry_acks(items) + + manager.send_unary_ack.assert_has_calls( + [ + mock.call( + ack_ids=["ack_id_string"], ack_reqs_dict={"ack_id_string": items[0]} + ), + mock.call( + ack_ids=["ack_id_string"], ack_reqs_dict={"ack_id_string": items[0]} + ), + mock.call( + ack_ids=["ack_id_string"], ack_reqs_dict={"ack_id_string": items[0]} + ), + ] + ) + + +def test_retry_modacks_in_new_thread(): + manager = mock.create_autospec( + streaming_pull_manager.StreamingPullManager, instance=True + ) + dispatcher_ = dispatcher.Dispatcher(manager, mock.sentinel.queue) + + f = futures.Future() + items = [requests.ModAckRequest(ack_id="ack_id_string", seconds=20, future=f,)] + # failure triggers creation of new retry thread + manager.send_unary_modack.side_effect = [([], items)] + with mock.patch("time.sleep", return_value=None): + with mock.patch.object(threading, "Thread", autospec=True) as Thread: + dispatcher_.modify_ack_deadline(items) + + assert len(Thread.mock_calls) == 2 + ctor_call = Thread.mock_calls[0] + assert ctor_call.kwargs["name"] == "Thread-RetryModAcks" + assert ctor_call.kwargs["target"].args[0] == items + assert ctor_call.kwargs["daemon"] + + +def test_retry_modacks(): + manager = mock.create_autospec( + streaming_pull_manager.StreamingPullManager, instance=True + ) + dispatcher_ = dispatcher.Dispatcher(manager, mock.sentinel.queue) + + f = futures.Future() + items = [requests.ModAckRequest(ack_id="ack_id_string", seconds=20, future=f,)] + # first and second calls fail, third one succeeds + manager.send_unary_modack.side_effect = [([], items), ([], items), (items, [])] + with mock.patch("time.sleep", return_value=None): + dispatcher_._retry_modacks(items) + + manager.send_unary_modack.assert_has_calls( + [ + mock.call( + modify_deadline_ack_ids=["ack_id_string"], + modify_deadline_seconds=[20], + ack_reqs_dict={"ack_id_string": items[0]}, + ), + mock.call( + modify_deadline_ack_ids=["ack_id_string"], + modify_deadline_seconds=[20], + ack_reqs_dict={"ack_id_string": items[0]}, + ), + mock.call( + modify_deadline_ack_ids=["ack_id_string"], + modify_deadline_seconds=[20], + ack_reqs_dict={"ack_id_string": items[0]}, + ), + ] + ) + + def test_lease(): manager = mock.create_autospec( streaming_pull_manager.StreamingPullManager, instance=True @@ -203,14 +349,21 @@ def test_nack(): dispatcher_ = dispatcher.Dispatcher(manager, mock.sentinel.queue) items = [ - requests.NackRequest(ack_id="ack_id_string", byte_size=10, ordering_key="") + requests.NackRequest( + ack_id="ack_id_string", byte_size=10, ordering_key="", future=None + ) ] + manager.send_unary_modack.return_value = (items, []) dispatcher_.nack(items) - manager.send.assert_called_once_with( - gapic_types.StreamingPullRequest( - modify_deadline_ack_ids=["ack_id_string"], modify_deadline_seconds=[0] - ) + manager.send_unary_modack.assert_called_once_with( + modify_deadline_ack_ids=["ack_id_string"], + modify_deadline_seconds=[0], + ack_reqs_dict={ + "ack_id_string": requests.ModAckRequest( + ack_id="ack_id_string", seconds=0, future=None + ) + }, ) @@ -220,13 +373,14 @@ def test_modify_ack_deadline(): ) dispatcher_ = dispatcher.Dispatcher(manager, mock.sentinel.queue) - items = [requests.ModAckRequest(ack_id="ack_id_string", seconds=60)] + items = [requests.ModAckRequest(ack_id="ack_id_string", seconds=60, future=None)] + manager.send_unary_modack.return_value = (items, []) dispatcher_.modify_ack_deadline(items) - manager.send.assert_called_once_with( - gapic_types.StreamingPullRequest( - modify_deadline_ack_ids=["ack_id_string"], modify_deadline_seconds=[60] - ) + manager.send_unary_modack.assert_called_once_with( + modify_deadline_ack_ids=["ack_id_string"], + modify_deadline_seconds=[60], + ack_reqs_dict={"ack_id_string": items[0]}, ) @@ -238,21 +392,22 @@ def test_modify_ack_deadline_splitting_large_payload(): items = [ # use realistic lengths for ACK IDs (max 176 bytes) - requests.ModAckRequest(ack_id=str(i).zfill(176), seconds=60) + requests.ModAckRequest(ack_id=str(i).zfill(176), seconds=60, future=None) for i in range(5001) ] + manager.send_unary_modack.return_value = (items, []) dispatcher_.modify_ack_deadline(items) - calls = manager.send.call_args_list + calls = manager.send_unary_modack.call_args_list assert len(calls) == 3 all_ack_ids = {item.ack_id for item in items} sent_ack_ids = collections.Counter() for call in calls: - message = call.args[0] - assert message._pb.ByteSize() <= 524288 # server-side limit (2**19) - sent_ack_ids.update(message.modify_deadline_ack_ids) + modack_ackids = call[1]["modify_deadline_ack_ids"] + assert len(modack_ackids) <= dispatcher._ACK_IDS_BATCH_SIZE + sent_ack_ids.update(modack_ackids) assert set(sent_ack_ids) == all_ack_ids # all messages should have been MODACK-ed assert sent_ack_ids.most_common(1)[0][1] == 1 # each message MODACK-ed exactly once diff --git a/tests/unit/pubsub_v1/subscriber/test_futures_subscriber.py b/tests/unit/pubsub_v1/subscriber/test_futures_subscriber.py index 5411674c0..9f71109e7 100644 --- a/tests/unit/pubsub_v1/subscriber/test_futures_subscriber.py +++ b/tests/unit/pubsub_v1/subscriber/test_futures_subscriber.py @@ -19,6 +19,10 @@ from google.cloud.pubsub_v1.subscriber import futures from google.cloud.pubsub_v1.subscriber._protocol import streaming_pull_manager +from google.cloud.pubsub_v1.subscriber.exceptions import ( + AcknowledgeError, + AcknowledgeStatus, +) class TestStreamingPullFuture(object): @@ -76,3 +80,30 @@ def test_cancel(self): manager.close.assert_called_once() assert future.cancelled() + + +class TestFuture(object): + def test_cancel(self): + future = futures.Future() + assert future.cancel() is False + + def test_cancelled(self): + future = futures.Future() + assert future.cancelled() is False + + def test_result_on_success(self): + future = futures.Future() + future.set_result(AcknowledgeStatus.SUCCESS) + assert future.result() == AcknowledgeStatus.SUCCESS + + def test_result_on_failure(self): + future = futures.Future() + future.set_exception( + AcknowledgeError( + AcknowledgeStatus.PERMISSION_DENIED, "Something bad happened." + ) + ) + with pytest.raises(AcknowledgeError) as e: + future.result() + assert e.value.error_code == AcknowledgeStatus.PERMISSION_DENIED + assert e.value.info == "Something bad happened." diff --git a/tests/unit/pubsub_v1/subscriber/test_leaser.py b/tests/unit/pubsub_v1/subscriber/test_leaser.py index f389e5205..890c3c947 100644 --- a/tests/unit/pubsub_v1/subscriber/test_leaser.py +++ b/tests/unit/pubsub_v1/subscriber/test_leaser.py @@ -102,7 +102,7 @@ def test_maintain_leases_inactive_manager(caplog): leaser_.maintain_leases() # Leases should still be maintained even if the manager is inactive. - manager.dispatcher.modify_ack_deadline.assert_called() + manager._send_lease_modacks.assert_called() assert "exiting" in caplog.text @@ -138,9 +138,11 @@ def test_maintain_leases_ack_ids(): leaser_.maintain_leases() - manager.dispatcher.modify_ack_deadline.assert_called_once_with( - [requests.ModAckRequest(ack_id="my ack id", seconds=10)] - ) + assert len(manager._send_lease_modacks.mock_calls) == 1 + call = manager._send_lease_modacks.mock_calls[0] + ack_ids = list(call.args[0]) + assert ack_ids == ["my ack id"] + assert call.args[1] == 10 def test_maintain_leases_no_ack_ids(): @@ -182,14 +184,11 @@ def test_maintain_leases_outdated_items(time): leaser_.maintain_leases() # ack2, ack3, and ack4 should be renewed. ack1 should've been dropped - modacks = manager.dispatcher.modify_ack_deadline.call_args.args[0] - expected = [ - requests.ModAckRequest(ack_id="ack2", seconds=10), - requests.ModAckRequest(ack_id="ack3", seconds=10), - requests.ModAckRequest(ack_id="ack4", seconds=10), - ] - # Use sorting to allow for ordering variance. - assert sorted(modacks) == sorted(expected) + assert len(manager._send_lease_modacks.mock_calls) == 1 + call = manager._send_lease_modacks.mock_calls[0] + ack_ids = list(call.args[0]) + assert ack_ids == ["ack2", "ack3", "ack4"] + assert call.args[1] == 10 manager.dispatcher.drop.assert_called_once_with( [requests.DropRequest(ack_id="ack1", byte_size=50, ordering_key="")] diff --git a/tests/unit/pubsub_v1/subscriber/test_message.py b/tests/unit/pubsub_v1/subscriber/test_message.py index e3c14c93c..f5c7bf3c7 100644 --- a/tests/unit/pubsub_v1/subscriber/test_message.py +++ b/tests/unit/pubsub_v1/subscriber/test_message.py @@ -23,6 +23,7 @@ from google.cloud.pubsub_v1.subscriber._protocol import requests from google.protobuf import timestamp_pb2 from google.pubsub_v1 import types as gapic_types +from google.cloud.pubsub_v1.subscriber.exceptions import AcknowledgeStatus RECEIVED = datetime.datetime(2012, 4, 21, 15, 0, tzinfo=datetime.timezone.utc) @@ -32,7 +33,14 @@ PUBLISHED_SECONDS = datetime_helpers.to_milliseconds(PUBLISHED) // 1000 -def create_message(data, ack_id="ACKID", delivery_attempt=0, ordering_key="", **attrs): +def create_message( + data, + ack_id="ACKID", + delivery_attempt=0, + ordering_key="", + exactly_once_delivery_enabled=False, + **attrs +): with mock.patch.object(time, "time") as time_: time_.return_value = RECEIVED_SECONDS gapic_pubsub_message = gapic_types.PubsubMessage( @@ -51,6 +59,7 @@ def create_message(data, ack_id="ACKID", delivery_attempt=0, ordering_key="", ** ack_id=ack_id, delivery_attempt=delivery_attempt, request_queue=queue.Queue(), + exactly_once_delivery_enabled_func=lambda: exactly_once_delivery_enabled, ) return msg @@ -127,6 +136,42 @@ def test_ack(): byte_size=30, time_to_ack=mock.ANY, ordering_key="", + future=None, + ) + ) + check_call_types(put, requests.AckRequest) + + +def test_ack_with_response_exactly_once_delivery_disabled(): + msg = create_message(b"foo", ack_id="bogus_ack_id") + with mock.patch.object(msg._request_queue, "put") as put: + future = msg.ack_with_response() + put.assert_called_once_with( + requests.AckRequest( + ack_id="bogus_ack_id", + byte_size=30, + time_to_ack=mock.ANY, + ordering_key="", + future=None, + ) + ) + assert future.result() == AcknowledgeStatus.SUCCESS + check_call_types(put, requests.AckRequest) + + +def test_ack_with_response_exactly_once_delivery_enabled(): + msg = create_message( + b"foo", ack_id="bogus_ack_id", exactly_once_delivery_enabled=True + ) + with mock.patch.object(msg._request_queue, "put") as put: + future = msg.ack_with_response() + put.assert_called_once_with( + requests.AckRequest( + ack_id="bogus_ack_id", + byte_size=30, + time_to_ack=mock.ANY, + ordering_key="", + future=future, ) ) check_call_types(put, requests.AckRequest) @@ -147,7 +192,30 @@ def test_modify_ack_deadline(): with mock.patch.object(msg._request_queue, "put") as put: msg.modify_ack_deadline(60) put.assert_called_once_with( - requests.ModAckRequest(ack_id="bogus_ack_id", seconds=60) + requests.ModAckRequest(ack_id="bogus_ack_id", seconds=60, future=None) + ) + check_call_types(put, requests.ModAckRequest) + + +def test_modify_ack_deadline_with_response_exactly_once_delivery_disabled(): + msg = create_message(b"foo", ack_id="bogus_ack_id") + with mock.patch.object(msg._request_queue, "put") as put: + future = msg.modify_ack_deadline_with_response(60) + put.assert_called_once_with( + requests.ModAckRequest(ack_id="bogus_ack_id", seconds=60, future=None) + ) + assert future.result() == AcknowledgeStatus.SUCCESS + check_call_types(put, requests.ModAckRequest) + + +def test_modify_ack_deadline_with_response_exactly_once_delivery_enabled(): + msg = create_message( + b"foo", ack_id="bogus_ack_id", exactly_once_delivery_enabled=True + ) + with mock.patch.object(msg._request_queue, "put") as put: + future = msg.modify_ack_deadline_with_response(60) + put.assert_called_once_with( + requests.ModAckRequest(ack_id="bogus_ack_id", seconds=60, future=future) ) check_call_types(put, requests.ModAckRequest) @@ -157,7 +225,36 @@ def test_nack(): with mock.patch.object(msg._request_queue, "put") as put: msg.nack() put.assert_called_once_with( - requests.NackRequest(ack_id="bogus_ack_id", byte_size=30, ordering_key="") + requests.NackRequest( + ack_id="bogus_ack_id", byte_size=30, ordering_key="", future=None + ) + ) + check_call_types(put, requests.NackRequest) + + +def test_nack_with_response_exactly_once_delivery_disabled(): + msg = create_message(b"foo", ack_id="bogus_ack_id") + with mock.patch.object(msg._request_queue, "put") as put: + future = msg.nack_with_response() + put.assert_called_once_with( + requests.NackRequest( + ack_id="bogus_ack_id", byte_size=30, ordering_key="", future=None + ) + ) + assert future.result() == AcknowledgeStatus.SUCCESS + check_call_types(put, requests.NackRequest) + + +def test_nack_with_response_exactly_once_delivery_enabled(): + msg = create_message( + b"foo", ack_id="bogus_ack_id", exactly_once_delivery_enabled=True + ) + with mock.patch.object(msg._request_queue, "put") as put: + future = msg.nack_with_response() + put.assert_called_once_with( + requests.NackRequest( + ack_id="bogus_ack_id", byte_size=30, ordering_key="", future=future + ) ) check_call_types(put, requests.NackRequest) diff --git a/tests/unit/pubsub_v1/subscriber/test_messages_on_hold.py b/tests/unit/pubsub_v1/subscriber/test_messages_on_hold.py index 797430e07..0d28ec447 100644 --- a/tests/unit/pubsub_v1/subscriber/test_messages_on_hold.py +++ b/tests/unit/pubsub_v1/subscriber/test_messages_on_hold.py @@ -21,7 +21,13 @@ def make_message(ack_id, ordering_key): proto_msg = gapic_types.PubsubMessage(data=b"Q", ordering_key=ordering_key) - return message.Message(proto_msg._pb, ack_id, 0, queue.Queue()) + return message.Message( + proto_msg._pb, + ack_id, + 0, + queue.Queue(), + exactly_once_delivery_enabled_func=lambda: False, # pragma: NO COVER + ) def test_init(): diff --git a/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py b/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py index 8e4f6daf0..9e8d6c5ed 100644 --- a/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py +++ b/tests/unit/pubsub_v1/subscriber/test_streaming_pull_manager.py @@ -33,8 +33,13 @@ from google.cloud.pubsub_v1.subscriber._protocol import messages_on_hold from google.cloud.pubsub_v1.subscriber._protocol import requests from google.cloud.pubsub_v1.subscriber._protocol import streaming_pull_manager +from google.cloud.pubsub_v1.subscriber import exceptions as subscriber_exceptions +from google.cloud.pubsub_v1.subscriber import futures from google.pubsub_v1 import types as gapic_types import grpc +from google.rpc import status_pb2 +from google.rpc import code_pb2 +from google.rpc import error_details_pb2 @pytest.mark.parametrize( @@ -122,6 +127,16 @@ def make_manager(**kwargs): ) +def complete_modify_ack_deadline_calls(dispatcher): + def complete_futures(*args, **kwargs): + modack_requests = args[0] + for req in modack_requests: + if req.future: + req.future.set_result(subscriber_exceptions.AcknowledgeStatus.SUCCESS) + + dispatcher.modify_ack_deadline.side_effect = complete_futures + + def fake_leaser_add(leaser, init_msg_count=0, assumed_msg_size=10): """Add a simplified fake add() method to a leaser instance. @@ -144,8 +159,11 @@ def test__obtain_ack_deadline_no_custom_flow_control_setting(): manager = make_manager() - # Make sure that max_duration_per_lease_extension is disabled. - manager._flow_control = types.FlowControl(max_duration_per_lease_extension=0) + # Make sure that min_duration_per_lease_extension and + # max_duration_per_lease_extension is disabled. + manager._flow_control = types.FlowControl( + min_duration_per_lease_extension=0, max_duration_per_lease_extension=0 + ) deadline = manager._obtain_ack_deadline(maybe_update=True) assert deadline == histogram.MIN_ACK_DEADLINE @@ -175,6 +193,20 @@ def test__obtain_ack_deadline_with_max_duration_per_lease_extension(): assert deadline == histogram.MIN_ACK_DEADLINE + 1 +def test__obtain_ack_deadline_with_min_duration_per_lease_extension(): + from google.cloud.pubsub_v1.subscriber._protocol import histogram + + manager = make_manager() + manager._flow_control = types.FlowControl( + min_duration_per_lease_extension=histogram.MAX_ACK_DEADLINE + ) + manager.ack_histogram.add(histogram.MIN_ACK_DEADLINE) # make p99 value small + + # The deadline configured in flow control should prevail. + deadline = manager._obtain_ack_deadline(maybe_update=True) + assert deadline == histogram.MAX_ACK_DEADLINE + + def test__obtain_ack_deadline_with_max_duration_per_lease_extension_too_low(): from google.cloud.pubsub_v1.subscriber._protocol import histogram @@ -182,13 +214,58 @@ def test__obtain_ack_deadline_with_max_duration_per_lease_extension_too_low(): manager._flow_control = types.FlowControl( max_duration_per_lease_extension=histogram.MIN_ACK_DEADLINE - 1 ) - manager.ack_histogram.add(histogram.MIN_ACK_DEADLINE * 3) # make p99 value large # The deadline configured in flow control should be adjusted to the minimum allowed. deadline = manager._obtain_ack_deadline(maybe_update=True) assert deadline == histogram.MIN_ACK_DEADLINE +def test__obtain_ack_deadline_with_min_duration_per_lease_extension_too_high(): + from google.cloud.pubsub_v1.subscriber._protocol import histogram + + manager = make_manager() + manager._flow_control = types.FlowControl( + min_duration_per_lease_extension=histogram.MAX_ACK_DEADLINE + 1 + ) + + # The deadline configured in flow control should be adjusted to the maximum allowed. + deadline = manager._obtain_ack_deadline(maybe_update=True) + assert deadline == histogram.MAX_ACK_DEADLINE + + +def test__obtain_ack_deadline_with_exactly_once_enabled(): + manager = make_manager() + manager._flow_control = types.FlowControl( + min_duration_per_lease_extension=0 # leave as default value + ) + manager._exactly_once_enabled = True + manager.ack_histogram.add( + 10 + ) # reduce p99 value below 60s min for exactly_once subscriptions + + deadline = manager._obtain_ack_deadline(maybe_update=True) + # Since the 60-second min ack_deadline value for exactly_once subscriptions + # seconds is higher than the histogram value, the deadline should be 60 sec. + assert deadline == 60 + + +def test__obtain_ack_deadline_with_min_duration_per_lease_extension_with_exactly_once_enabled(): + from google.cloud.pubsub_v1.subscriber._protocol import histogram + + manager = make_manager() + manager._flow_control = types.FlowControl( + min_duration_per_lease_extension=histogram.MAX_ACK_DEADLINE + ) + manager._exactly_once_enabled = True + manager.ack_histogram.add(histogram.MIN_ACK_DEADLINE) # make p99 value small + + # The deadline configured in flow control should prevail. + deadline = manager._obtain_ack_deadline(maybe_update=True) + # User-defined custom min ack_deadline value takes precedence over + # exactly_once default of 60 seconds. + assert deadline == histogram.MAX_ACK_DEADLINE + + def test__obtain_ack_deadline_no_value_update(): manager = make_manager() @@ -401,6 +478,7 @@ def test__maybe_release_messages_negative_on_hold_bytes_warning(caplog): manager = make_manager( flow_control=types.FlowControl(max_messages=10, max_bytes=1000) ) + manager._callback = lambda msg: msg # pragma: NO COVER msg = mock.create_autospec(message.Message, instance=True, ack_id="ack", size=17) manager._messages_on_hold.put(msg) @@ -425,21 +503,38 @@ def test__maybe_release_messages_negative_on_hold_bytes_warning(caplog): assert manager._on_hold_bytes == 0 # should be auto-corrected -def test_send_unary(): +def test_send_unary_ack(): manager = make_manager() - manager.send( - gapic_types.StreamingPullRequest( - ack_ids=["ack_id1", "ack_id2"], - modify_deadline_ack_ids=["ack_id3", "ack_id4", "ack_id5"], - modify_deadline_seconds=[10, 20, 20], - ) - ) + ack_reqs_dict = { + "ack_id1": requests.AckRequest( + ack_id="ack_id1", byte_size=0, time_to_ack=20, ordering_key="", future=None + ), + "ack_id2": requests.AckRequest( + ack_id="ack_id2", byte_size=0, time_to_ack=20, ordering_key="", future=None + ), + } + manager.send_unary_ack(ack_ids=["ack_id1", "ack_id2"], ack_reqs_dict=ack_reqs_dict) manager._client.acknowledge.assert_called_once_with( subscription=manager._subscription, ack_ids=["ack_id1", "ack_id2"] ) + +def test_send_unary_modack(): + manager = make_manager() + + ack_reqs_dict = { + "ack_id3": requests.ModAckRequest(ack_id="ack_id3", seconds=60, future=None), + "ack_id4": requests.ModAckRequest(ack_id="ack_id4", seconds=60, future=None), + "ack_id5": requests.ModAckRequest(ack_id="ack_id5", seconds=60, future=None), + } + manager.send_unary_modack( + modify_deadline_ack_ids=["ack_id3", "ack_id4", "ack_id5"], + modify_deadline_seconds=[10, 20, 20], + ack_reqs_dict=ack_reqs_dict, + ) + manager._client.modify_ack_deadline.assert_has_calls( [ mock.call( @@ -457,29 +552,61 @@ def test_send_unary(): ) -def test_send_unary_empty(): +def test_send_unary_ack_api_call_error(caplog): + caplog.set_level(logging.DEBUG) + manager = make_manager() - manager.send(gapic_types.StreamingPullRequest()) + error = exceptions.GoogleAPICallError("The front fell off") + manager._client.acknowledge.side_effect = error - manager._client.acknowledge.assert_not_called() - manager._client.modify_ack_deadline.assert_not_called() + ack_reqs_dict = { + "ack_id1": requests.AckRequest( + ack_id="ack_id1", byte_size=0, time_to_ack=20, ordering_key="", future=None + ), + "ack_id2": requests.AckRequest( + ack_id="ack_id2", byte_size=0, time_to_ack=20, ordering_key="", future=None + ), + } + manager.send_unary_ack(ack_ids=["ack_id1", "ack_id2"], ack_reqs_dict=ack_reqs_dict) + + assert "The front fell off" in caplog.text -def test_send_unary_api_call_error(caplog): +def test_send_unary_modack_api_call_error(caplog): caplog.set_level(logging.DEBUG) manager = make_manager() error = exceptions.GoogleAPICallError("The front fell off") - manager._client.acknowledge.side_effect = error - - manager.send(gapic_types.StreamingPullRequest(ack_ids=["ack_id1", "ack_id2"])) + manager._client.modify_ack_deadline.side_effect = error + + ack_reqs_dict = { + "ack_id1": requests.AckRequest( + ack_id="ack_id1", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=futures.Future(), + ), + "ack_id2": requests.AckRequest( + ack_id="ack_id2", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=futures.Future(), + ), + } + manager.send_unary_modack( + modify_deadline_ack_ids=["ack_id_string"], + modify_deadline_seconds=[0], + ack_reqs_dict=ack_reqs_dict, + ) assert "The front fell off" in caplog.text -def test_send_unary_retry_error(caplog): +def test_send_unary_ack_retry_error(caplog): caplog.set_level(logging.DEBUG) manager, _, _, _, _, _ = make_running_manager() @@ -489,11 +616,68 @@ def test_send_unary_retry_error(caplog): ) manager._client.acknowledge.side_effect = error + future1 = futures.Future() + future2 = futures.Future() + ack_reqs_dict = { + "ack_id1": requests.AckRequest( + ack_id="ack_id1", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=future1, + ), + "ack_id2": requests.AckRequest( + ack_id="ack_id2", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=future2, + ), + } + with pytest.raises(exceptions.RetryError): + manager.send_unary_ack( + ack_ids=["ack_id1", "ack_id2"], ack_reqs_dict=ack_reqs_dict + ) + + assert "RetryError while sending unary RPC" in caplog.text + assert "signaled streaming pull manager shutdown" in caplog.text + assert isinstance(future1.exception(), subscriber_exceptions.AcknowledgeError) + assert ( + future1.exception().error_code is subscriber_exceptions.AcknowledgeStatus.OTHER + ) + assert isinstance(future2.exception(), subscriber_exceptions.AcknowledgeError) + assert ( + future2.exception().error_code is subscriber_exceptions.AcknowledgeStatus.OTHER + ) + + +def test_send_unary_modack_retry_error(caplog): + caplog.set_level(logging.DEBUG) + + manager, _, _, _, _, _ = make_running_manager() + + error = exceptions.RetryError( + "Too long a transient error", cause=Exception("Out of time!") + ) + manager._client.modify_ack_deadline.side_effect = error + + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.ModAckRequest(ack_id="ackid1", seconds=60, future=future) + } with pytest.raises(exceptions.RetryError): - manager.send(gapic_types.StreamingPullRequest(ack_ids=["ack_id1", "ack_id2"])) + manager.send_unary_modack( + modify_deadline_ack_ids=["ackid1"], + modify_deadline_seconds=[0], + ack_reqs_dict=ack_reqs_dict, + ) assert "RetryError while sending unary RPC" in caplog.text assert "signaled streaming pull manager shutdown" in caplog.text + assert isinstance(future.exception(), subscriber_exceptions.AcknowledgeError) + assert ( + future.exception().error_code is subscriber_exceptions.AcknowledgeStatus.OTHER + ) def test_heartbeat(): @@ -518,6 +702,23 @@ def test_heartbeat_inactive(): assert not result +def test_heartbeat_stream_ack_deadline_seconds(): + manager = make_manager() + manager._rpc = mock.create_autospec(bidi.BidiRpc, instance=True) + manager._rpc.is_active = True + # Send new ack deadline with next heartbeat. + manager._send_new_ack_deadline = True + + result = manager.heartbeat() + + manager._rpc.send.assert_called_once_with( + gapic_types.StreamingPullRequest(stream_ack_deadline_seconds=10) + ) + assert result + # Set to false after a send is initiated. + assert not manager._send_new_ack_deadline + + @mock.patch("google.api_core.bidi.ResumableBidiRpc", autospec=True) @mock.patch("google.api_core.bidi.BackgroundConsumer", autospec=True) @mock.patch("google.cloud.pubsub_v1.subscriber._protocol.leaser.Leaser", autospec=True) @@ -552,7 +753,7 @@ def test_open(heartbeater, dispatcher, leaser, background_consumer, resumable_bi assert manager._consumer == background_consumer.return_value resumable_bidi_rpc.assert_called_once_with( - start_rpc=manager._client.api.streaming_pull, + start_rpc=manager._client.streaming_pull, initial_request=mock.ANY, should_recover=manager._should_recover, should_terminate=manager._should_terminate, @@ -561,7 +762,7 @@ def test_open(heartbeater, dispatcher, leaser, background_consumer, resumable_bi initial_request_arg = resumable_bidi_rpc.call_args.kwargs["initial_request"] assert initial_request_arg.func == manager._get_initial_request assert initial_request_arg.args[0] == 18 - assert not manager._client.api.get_subscription.called + assert not manager._client.get_subscription.called resumable_bidi_rpc.return_value.add_done_callback.assert_called_once_with( manager._on_rpc_done @@ -859,7 +1060,127 @@ def test__on_response_modifies_ack_deadline(): manager._on_response(response) dispatcher.modify_ack_deadline.assert_called_once_with( - [requests.ModAckRequest("ack_1", 18), requests.ModAckRequest("ack_2", 18)] + [ + requests.ModAckRequest("ack_1", 18, None), + requests.ModAckRequest("ack_2", 18, None), + ] + ) + + +def test__on_response_modifies_ack_deadline_with_exactly_once_min_lease(): + # exactly_once is disabled by default. + manager, _, dispatcher, leaser, _, scheduler = make_running_manager() + manager._callback = mock.sentinel.callback + complete_modify_ack_deadline_calls(dispatcher) + + # make p99 value smaller than exactly_once min lease + manager.ack_histogram.add(10) + + # adjust message bookkeeping in leaser + fake_leaser_add(leaser, init_msg_count=0, assumed_msg_size=42) + + # Set up the response with the first set of messages and exactly_once not + # enabled. + response1 = gapic_types.StreamingPullResponse( + received_messages=[ + gapic_types.ReceivedMessage( + ack_id="ack_1", + message=gapic_types.PubsubMessage(data=b"foo", message_id="1"), + ), + gapic_types.ReceivedMessage( + ack_id="ack_2", + message=gapic_types.PubsubMessage(data=b"bar", message_id="2"), + ), + ], + subscription_properties=gapic_types.StreamingPullResponse.SubscriptionProperties( + exactly_once_delivery_enabled=False + ), + ) + + # Set up the response with the second set of messages and exactly_once enabled. + response2 = gapic_types.StreamingPullResponse( + received_messages=[ + gapic_types.ReceivedMessage( + ack_id="ack_3", + message=gapic_types.PubsubMessage(data=b"foo", message_id="1"), + ), + gapic_types.ReceivedMessage( + ack_id="ack_4", + message=gapic_types.PubsubMessage(data=b"bar", message_id="2"), + ), + ], + subscription_properties=gapic_types.StreamingPullResponse.SubscriptionProperties( + exactly_once_delivery_enabled=True + ), + ) + + # exactly_once is still disabled b/c subscription_properties says so + manager._on_response(response1) + + # expect mod-acks are called with histogram-based lease value + assert len(dispatcher.modify_ack_deadline.mock_calls) == 1 + call = dispatcher.modify_ack_deadline.mock_calls[0] + assert call.args[0] == [ + requests.ModAckRequest("ack_1", 10, None), + requests.ModAckRequest("ack_2", 10, None), + ] + + # exactly_once should be enabled after this request b/c subscription_properties says so + manager._on_response(response2) + + # expect mod-acks called with 60 sec min lease value for exactly_once subscriptions + # ignore the futures here + assert len(dispatcher.modify_ack_deadline.mock_calls) == 2 + call = dispatcher.modify_ack_deadline.mock_calls[1] + modack_reqs = call.args[0] + assert modack_reqs[0].ack_id == "ack_3" + assert modack_reqs[0].seconds == 60 + assert modack_reqs[1].ack_id == "ack_4" + assert modack_reqs[1].seconds == 60 + + +def test__on_response_send_ack_deadline_after_enabling_exactly_once(): + # exactly_once is disabled by default. + manager, _, dispatcher, leaser, _, scheduler = make_running_manager() + manager._callback = mock.sentinel.callback + complete_modify_ack_deadline_calls(dispatcher) + + # set up an active RPC + manager._rpc = mock.create_autospec(bidi.BidiRpc, instance=True) + manager._rpc.is_active = True + + # make p99 value smaller than exactly_once min lease + manager.ack_histogram.add(10) + + # adjust message bookkeeping in leaser + fake_leaser_add(leaser, init_msg_count=0, assumed_msg_size=42) + + # Set up the response with the a message and exactly_once enabled. + response2 = gapic_types.StreamingPullResponse( + received_messages=[ + gapic_types.ReceivedMessage( + ack_id="ack_1", + message=gapic_types.PubsubMessage(data=b"foo", message_id="1"), + ) + ], + subscription_properties=gapic_types.StreamingPullResponse.SubscriptionProperties( + exactly_once_delivery_enabled=True + ), + ) + + # exactly_once should be enabled after this request b/c subscription_properties says so + # when exactly_once is enabled or disabled, we send a new ack_deadline via + # the heartbeat + # should satisfy assertion 1 + manager._on_response(response2) + + # simulate periodic heartbeat trigger + heartbeat_request_sent = manager.heartbeat() + assert heartbeat_request_sent + + # heartbeat request is sent with the 60 sec min lease value for exactly_once subscriptions + manager._rpc.send.assert_called_once_with( + gapic_types.StreamingPullRequest(stream_ack_deadline_seconds=60) ) @@ -889,7 +1210,10 @@ def test__on_response_no_leaser_overload(): manager._on_response(response) dispatcher.modify_ack_deadline.assert_called_once_with( - [requests.ModAckRequest("fack", 10), requests.ModAckRequest("back", 10)] + [ + requests.ModAckRequest("fack", 10, None), + requests.ModAckRequest("back", 10, None), + ] ) schedule_calls = scheduler.schedule.mock_calls @@ -936,9 +1260,9 @@ def test__on_response_with_leaser_overload(): # deadline extended, even those not dispatched to callbacks dispatcher.modify_ack_deadline.assert_called_once_with( [ - requests.ModAckRequest("fack", 10), - requests.ModAckRequest("back", 10), - requests.ModAckRequest("zack", 10), + requests.ModAckRequest("fack", 10, None), + requests.ModAckRequest("back", 10, None), + requests.ModAckRequest("zack", 10, None), ] ) @@ -1016,9 +1340,9 @@ def test__on_response_with_ordering_keys(): # deadline extended, even those not dispatched to callbacks. dispatcher.modify_ack_deadline.assert_called_once_with( [ - requests.ModAckRequest("fack", 10), - requests.ModAckRequest("back", 10), - requests.ModAckRequest("zack", 10), + requests.ModAckRequest("fack", 10, None), + requests.ModAckRequest("back", 10, None), + requests.ModAckRequest("zack", 10, None), ] ) @@ -1057,6 +1381,109 @@ def test__on_response_with_ordering_keys(): assert manager._messages_on_hold.get() is None +def test__on_response_enable_exactly_once(): + manager, _, dispatcher, leaser, _, scheduler = make_running_manager() + manager._callback = mock.sentinel.callback + complete_modify_ack_deadline_calls(dispatcher) + + # Set up the messages. + response = gapic_types.StreamingPullResponse( + received_messages=[ + gapic_types.ReceivedMessage( + ack_id="fack", + message=gapic_types.PubsubMessage(data=b"foo", message_id="1"), + ) + ], + subscription_properties=gapic_types.StreamingPullResponse.SubscriptionProperties( + exactly_once_delivery_enabled=True + ), + ) + + # adjust message bookkeeping in leaser + fake_leaser_add(leaser, init_msg_count=0, assumed_msg_size=42) + + # exactly_once should be enabled + manager._on_response(response) + + assert manager._exactly_once_delivery_enabled() + # new deadline for exactly_once subscriptions should be used + assert manager.ack_deadline == 60 + + +def test__on_response_disable_exactly_once(): + from google.cloud.pubsub_v1.subscriber._protocol import histogram + + manager, _, dispatcher, leaser, _, scheduler = make_running_manager() + manager._callback = mock.sentinel.callback + + manager._flow_control = types.FlowControl( + min_duration_per_lease_extension=histogram.MIN_ACK_DEADLINE + ) + # enable exactly_once + manager._exactly_once_enabled = True + + # Set up the messages. + response = gapic_types.StreamingPullResponse( + received_messages=[ + gapic_types.ReceivedMessage( + ack_id="fack", + message=gapic_types.PubsubMessage(data=b"foo", message_id="1"), + ) + ], + subscription_properties=gapic_types.StreamingPullResponse.SubscriptionProperties( + exactly_once_delivery_enabled=False + ), + ) + + # adjust message bookkeeping in leaser + fake_leaser_add(leaser, init_msg_count=0, assumed_msg_size=42) + + # exactly_once should be disabled + manager._on_response(response) + + assert not manager._exactly_once_enabled + # The deadline configured in flow control should be used, not the + # exactly_once minimum since exactly_once has been disabled. + deadline = manager._obtain_ack_deadline(maybe_update=True) + assert deadline == histogram.MIN_ACK_DEADLINE + + +def test__on_response_exactly_once_immediate_modacks_fail(): + manager, _, dispatcher, leaser, _, scheduler = make_running_manager() + manager._callback = mock.sentinel.callback + + def complete_futures_with_error(*args, **kwargs): + modack_requests = args[0] + for req in modack_requests: + req.future.set_exception( + subscriber_exceptions.AcknowledgeError( + subscriber_exceptions.AcknowledgeStatus.SUCCESS, None + ) + ) + + dispatcher.modify_ack_deadline.side_effect = complete_futures_with_error + + # Set up the messages. + response = gapic_types.StreamingPullResponse( + received_messages=[ + gapic_types.ReceivedMessage( + ack_id="fack", + message=gapic_types.PubsubMessage(data=b"foo", message_id="1"), + ) + ], + subscription_properties=gapic_types.StreamingPullResponse.SubscriptionProperties( + exactly_once_delivery_enabled=True + ), + ) + + # adjust message bookkeeping in leaser + fake_leaser_add(leaser, init_msg_count=0, assumed_msg_size=42) + + # exactly_once should be enabled + manager._on_response(response) + # exceptions are logged, but otherwise no effect + + def test__should_recover_true(): manager = make_manager() @@ -1129,3 +1556,369 @@ def test_activate_ordering_keys_stopped_scheduler(): manager.activate_ordering_keys(["key1", "key2"]) manager._messages_on_hold.activate_ordering_keys.assert_not_called() + + +@mock.patch("grpc_status.rpc_status.from_call") +@mock.patch("google.protobuf.any_pb2.Any.Unpack") +def test_get_ack_errors_unable_to_unpack(from_call, unpack): + st = status_pb2.Status() + st.code = code_pb2.Code.INTERNAL + st.message = "qmsg" + error_info = error_details_pb2.ErrorInfo() + error_info.metadata["ack_1"] = "error1" + st.details.add().Pack(error_info) + mock_gprc_call = mock.Mock(spec=grpc.Call) + exception = exceptions.InternalServerError( + "msg", errors=(), response=mock_gprc_call + ) + from_call.return_value = st + # Unpack() failed + unpack.return_value = None + + assert not streaming_pull_manager._get_ack_errors(exception) + + +@mock.patch("grpc_status.rpc_status.from_call") +def test_get_ack_errors_no_response_obj(from_call): + exception = exceptions.InternalServerError("msg", errors=(), response=None) + # No response obj + assert not streaming_pull_manager._get_ack_errors(exception) + + +@mock.patch("grpc_status.rpc_status.from_call") +def test_get_ack_errors_from_call_returned_none(from_call): + mock_gprc_call = mock.Mock(spec=grpc.Call) + exception = exceptions.InternalServerError( + "msg", errors=(), response=mock_gprc_call + ) + from_call.return_value = None + # rpc_status.from_call() returned None + assert not streaming_pull_manager._get_ack_errors(exception) + + +@mock.patch("grpc_status.rpc_status.from_call") +def test_get_ack_errors_value_error_thrown(from_call): + mock_gprc_call = mock.Mock(spec=grpc.Call) + exception = exceptions.InternalServerError( + "msg", errors=(), response=mock_gprc_call + ) + from_call.side_effect = ValueError("val error msg") + # ValueError thrown, so return None + assert not streaming_pull_manager._get_ack_errors(exception) + + +@mock.patch("grpc_status.rpc_status.from_call") +def test_get_ack_errors_no_error_details(from_call): + st = status_pb2.Status() + st.code = code_pb2.Code.INTERNAL + st.message = "qmsg" + mock_gprc_call = mock.Mock(spec=grpc.Call) + exception = exceptions.InternalServerError( + "msg", errors=(), response=mock_gprc_call + ) + from_call.side_effect = None + from_call.return_value = st + # status has no details to extract exactly-once error info from + assert not streaming_pull_manager._get_ack_errors(exception) + + +@mock.patch("grpc_status.rpc_status.from_call") +def test_get_ack_errors_detail_not_error_info(from_call): + st = status_pb2.Status() + st.code = code_pb2.Code.INTERNAL + st.message = "qmsg" + # pack a dummy status instead of an ErrorInfo + dummy_status = status_pb2.Status() + st.details.add().Pack(dummy_status) + mock_gprc_call = mock.Mock(spec=grpc.Call) + exception = exceptions.InternalServerError( + "msg", errors=(), response=mock_gprc_call + ) + from_call.side_effect = None + from_call.return_value = st + assert not streaming_pull_manager._get_ack_errors(exception) + + +@mock.patch("grpc_status.rpc_status.from_call") +def test_get_ack_errors_happy_case(from_call): + st = status_pb2.Status() + st.code = code_pb2.Code.INTERNAL + st.message = "qmsg" + error_info = error_details_pb2.ErrorInfo() + error_info.metadata["ack_1"] = "error1" + st.details.add().Pack(error_info) + mock_gprc_call = mock.Mock(spec=grpc.Call) + exception = exceptions.InternalServerError( + "msg", errors=(), response=mock_gprc_call + ) + from_call.side_effect = None + from_call.return_value = st + # happy case - errors returned in a map + ack_errors = streaming_pull_manager._get_ack_errors(exception) + assert ack_errors + assert ack_errors["ack_1"] == "error1" + + +def test_process_requests_no_requests(): + # no requests so no items in results lists + ack_reqs_dict = {} + errors_dict = {} + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + assert not requests_completed + assert not requests_to_retry + + +def test_process_requests_error_dict_is_none(): + # it's valid to pass in `None` for `errors_dict` + ack_reqs_dict = {} + errors_dict = None + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + assert not requests_completed + assert not requests_to_retry + + +def test_process_requests_no_errors_has_no_future(): + # no errors so request should be completed, even with no future + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=None + ) + } + errors_dict = {} + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + assert requests_completed[0].ack_id == "ackid1" + assert not requests_to_retry + + +def test_process_requests_no_errors(): + # no errors so request and its future should be completed + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=future + ) + } + errors_dict = {} + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + assert requests_completed[0].ack_id == "ackid1" + assert future.result() == subscriber_exceptions.AcknowledgeStatus.SUCCESS + assert not requests_to_retry + + +def test_process_requests_permanent_error_raises_exception(): + # a permanent error raises an exception + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=future + ) + } + errors_dict = {"ackid1": "PERMANENT_FAILURE_INVALID_ACK_ID"} + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + assert requests_completed[0].ack_id == "ackid1" + with pytest.raises(subscriber_exceptions.AcknowledgeError) as exc_info: + future.result() + assert ( + exc_info.value.error_code + == subscriber_exceptions.AcknowledgeStatus.INVALID_ACK_ID + ) + assert not requests_to_retry + + +def test_process_requests_transient_error_returns_request(): + # a transient error returns the request in `requests_to_retry` + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=future + ) + } + errors_dict = {"ackid1": "TRANSIENT_FAILURE_INVALID_ACK_ID"} + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + assert not requests_completed + assert requests_to_retry[0].ack_id == "ackid1" + assert not future.done() + + +def test_process_requests_unknown_error_raises_exception(): + # an unknown error raises an exception + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=future + ) + } + errors_dict = {"ackid1": "unknown_error"} + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + assert requests_completed[0].ack_id == "ackid1" + with pytest.raises(subscriber_exceptions.AcknowledgeError) as exc_info: + future.result() + assert exc_info.value.error_code == subscriber_exceptions.AcknowledgeStatus.OTHER + assert exc_info.value.info == "unknown_error" + assert not requests_to_retry + + +def test_process_requests_permission_denied_error_status_raises_exception(): + # a permission-denied error status raises an exception + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=future + ) + } + st = status_pb2.Status() + st.code = code_pb2.Code.PERMISSION_DENIED + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + st, ack_reqs_dict, None + ) + assert requests_completed[0].ack_id == "ackid1" + with pytest.raises(subscriber_exceptions.AcknowledgeError) as exc_info: + future.result() + assert ( + exc_info.value.error_code + == subscriber_exceptions.AcknowledgeStatus.PERMISSION_DENIED + ) + assert exc_info.value.info is None + assert not requests_to_retry + + +def test_process_requests_failed_precondition_error_status_raises_exception(): + # a failed-precondition error status raises an exception + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=future + ) + } + st = status_pb2.Status() + st.code = code_pb2.Code.FAILED_PRECONDITION + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + st, ack_reqs_dict, None + ) + assert requests_completed[0].ack_id == "ackid1" + with pytest.raises(subscriber_exceptions.AcknowledgeError) as exc_info: + future.result() + assert ( + exc_info.value.error_code + == subscriber_exceptions.AcknowledgeStatus.FAILED_PRECONDITION + ) + assert exc_info.value.info is None + assert not requests_to_retry + + +def test_process_requests_other_error_status_raises_exception(): + # an unrecognized error status raises an exception + future = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", byte_size=0, time_to_ack=20, ordering_key="", future=future + ) + } + st = status_pb2.Status() + st.code = code_pb2.Code.OUT_OF_RANGE + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + st, ack_reqs_dict, None + ) + assert requests_completed[0].ack_id == "ackid1" + with pytest.raises(subscriber_exceptions.AcknowledgeError) as exc_info: + future.result() + assert exc_info.value.error_code == subscriber_exceptions.AcknowledgeStatus.OTHER + assert not requests_to_retry + + +def test_process_requests_mixed_success_and_failure_acks(): + # mixed success and failure (acks) + future1 = futures.Future() + future2 = futures.Future() + future3 = futures.Future() + ack_reqs_dict = { + "ackid1": requests.AckRequest( + ack_id="ackid1", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=future1, + ), + "ackid2": requests.AckRequest( + ack_id="ackid2", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=future2, + ), + "ackid3": requests.AckRequest( + ack_id="ackid3", + byte_size=0, + time_to_ack=20, + ordering_key="", + future=future3, + ), + } + errors_dict = { + "ackid1": "PERMANENT_FAILURE_INVALID_ACK_ID", + "ackid2": "TRANSIENT_FAILURE_INVALID_ACK_ID", + } + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + # message with ack_id 'ackid1' fails with an exception + assert requests_completed[0].ack_id == "ackid1" + with pytest.raises(subscriber_exceptions.AcknowledgeError) as exc_info: + future1.result() + assert ( + exc_info.value.error_code + == subscriber_exceptions.AcknowledgeStatus.INVALID_ACK_ID + ) + # message with ack_id 'ackid2' is to be retried + assert requests_to_retry[0].ack_id == "ackid2" + assert not requests_to_retry[0].future.done() + # message with ack_id 'ackid3' succeeds + assert requests_completed[1].ack_id == "ackid3" + assert future3.result() == subscriber_exceptions.AcknowledgeStatus.SUCCESS + + +def test_process_requests_mixed_success_and_failure_modacks(): + # mixed success and failure (modacks) + future1 = futures.Future() + future2 = futures.Future() + future3 = futures.Future() + ack_reqs_dict = { + "ackid1": requests.ModAckRequest(ack_id="ackid1", seconds=60, future=future1), + "ackid2": requests.ModAckRequest(ack_id="ackid2", seconds=60, future=future2), + "ackid3": requests.ModAckRequest(ack_id="ackid3", seconds=60, future=future3), + } + errors_dict = { + "ackid1": "PERMANENT_FAILURE_INVALID_ACK_ID", + "ackid2": "TRANSIENT_FAILURE_INVALID_ACK_ID", + } + requests_completed, requests_to_retry = streaming_pull_manager._process_requests( + None, ack_reqs_dict, errors_dict + ) + # message with ack_id 'ackid1' fails with an exception + assert requests_completed[0].ack_id == "ackid1" + with pytest.raises(subscriber_exceptions.AcknowledgeError) as exc_info: + future1.result() + assert ( + exc_info.value.error_code + == subscriber_exceptions.AcknowledgeStatus.INVALID_ACK_ID + ) + # message with ack_id 'ackid2' is to be retried + assert requests_to_retry[0].ack_id == "ackid2" + assert not requests_to_retry[0].future.done() + # message with ack_id 'ackid3' succeeds + assert requests_completed[1].ack_id == "ackid3" + assert future3.result() == subscriber_exceptions.AcknowledgeStatus.SUCCESS diff --git a/tests/unit/pubsub_v1/subscriber/test_subscriber_client.py b/tests/unit/pubsub_v1/subscriber/test_subscriber_client.py index 601b40bcc..793ceca3c 100644 --- a/tests/unit/pubsub_v1/subscriber/test_subscriber_client.py +++ b/tests/unit/pubsub_v1/subscriber/test_subscriber_client.py @@ -26,18 +26,13 @@ from google.pubsub_v1.services.subscriber.transports.grpc import SubscriberGrpcTransport -def test_init(creds): - client = subscriber.Client(credentials=creds) - assert isinstance(client.api, subscriber_client.SubscriberClient) - - def test_init_default_client_info(creds): client = subscriber.Client(credentials=creds) installed_version = subscriber.client.__version__ expected_client_info = f"gccl/{installed_version}" - for wrapped_method in client.api.transport._wrapped_methods.values(): + for wrapped_method in client.transport._wrapped_methods.values(): user_agent = next( ( header_value @@ -58,16 +53,14 @@ def test_init_default_closed_state(creds): def test_init_w_custom_transport(creds): transport = SubscriberGrpcTransport(credentials=creds) client = subscriber.Client(transport=transport) - assert isinstance(client.api, subscriber_client.SubscriberClient) - assert client.api._transport is transport + assert client._transport is transport def test_init_w_api_endpoint(creds): client_options = {"api_endpoint": "testendpoint.google.com"} client = subscriber.Client(client_options=client_options, credentials=creds) - assert isinstance(client.api, subscriber_client.SubscriberClient) - assert (client.api._transport.grpc_channel._channel.target()).decode( + assert (client._transport.grpc_channel._channel.target()).decode( "utf-8" ) == "testendpoint.google.com:443" @@ -75,8 +68,7 @@ def test_init_w_api_endpoint(creds): def test_init_w_empty_client_options(creds): client = subscriber.Client(client_options={}, credentials=creds) - assert isinstance(client.api, subscriber_client.SubscriberClient) - assert (client.api._transport.grpc_channel._channel.target()).decode( + assert (client._transport.grpc_channel._channel.target()).decode( "utf-8" ) == subscriber_client.SubscriberClient.SERVICE_ADDRESS @@ -98,12 +90,12 @@ def init(self, *args, **kwargs): "credentials_file": "file.json", } ) - client_options = client._api.kwargs["client_options"] + client_options = client.kwargs["client_options"] assert client_options.get("quota_project_id") == "42" assert client_options.get("scopes") == [] assert client_options.get("credentials_file") == "file.json" assert client.target == "testendpoint.google.com" - assert client.api.transport._ssl_channel_credentials == mock_ssl_creds + assert client.transport._ssl_channel_credentials == mock_ssl_creds def test_init_emulator(monkeypatch): @@ -116,7 +108,7 @@ def test_init_emulator(monkeypatch): # # Sadly, there seems to be no good way to do this without poking at # the private API of gRPC. - channel = client.api._transport.pull._channel + channel = client._transport.pull._channel assert channel.target().decode("utf8") == "/baz/bacon:123" @@ -184,7 +176,7 @@ def test_subscribe_options(manager_open, creds): def test_close(creds): client = subscriber.Client(credentials=creds) - patcher = mock.patch.object(client.api._transport.grpc_channel, "close") + patcher = mock.patch.object(client._transport.grpc_channel, "close") with patcher as patched_close: client.close() @@ -195,7 +187,7 @@ def test_close(creds): def test_closes_channel_as_context_manager(creds): client = subscriber.Client(credentials=creds) - patcher = mock.patch.object(client.api._transport.grpc_channel, "close") + patcher = mock.patch.object(client._transport.grpc_channel, "close") with patcher as patched_close: with client: @@ -207,13 +199,45 @@ def test_closes_channel_as_context_manager(creds): def test_context_manager_raises_if_closed(creds): client = subscriber.Client(credentials=creds) - with mock.patch.object(client.api._transport.grpc_channel, "close"): + with mock.patch.object(client._transport.grpc_channel, "close"): client.close() expetect_msg = r"(?i).*closed.*cannot.*context manager.*" with pytest.raises(RuntimeError, match=expetect_msg): with client: - pass + pass # pragma: NO COVER + + +def test_api_property_deprecated(creds): + client = subscriber.Client(credentials=creds) + + with warnings.catch_warnings(record=True) as warned: + client.api + + assert len(warned) == 1 + assert issubclass(warned[0].category, DeprecationWarning) + warning_msg = str(warned[0].message) + assert "client.api" in warning_msg + + +def test_api_property_proxy_to_generated_client(creds): + client = subscriber.Client(credentials=creds) + + with warnings.catch_warnings(record=True): + api_object = client.api + + # Not a perfect check, but we are satisficed if the returned API object indeed + # contains all methods of the generated class. + superclass_attrs = (attr for attr in dir(type(client).__mro__[1])) + assert all( + hasattr(api_object, attr) + for attr in superclass_attrs + if callable(getattr(client, attr)) + ) + + # The close() method only exists on the hand-written wrapper class. + assert hasattr(client, "close") + assert not hasattr(api_object, "close") def test_streaming_pull_gapic_monkeypatch(creds): @@ -222,7 +246,7 @@ def test_streaming_pull_gapic_monkeypatch(creds): with mock.patch("google.api_core.gapic_v1.method.wrap_method"): client.streaming_pull(requests=iter([])) - transport = client.api._transport + transport = client._transport assert hasattr(transport.streaming_pull, "_prefetch_first_result_") assert not transport.streaming_pull._prefetch_first_result_ @@ -232,7 +256,7 @@ def test_sync_pull_warning_if_return_immediately(creds): subscription_path = "projects/foo/subscriptions/bar" with mock.patch.object( - client.api._transport, "_wrapped_methods" + client._transport, "_wrapped_methods" ), warnings.catch_warnings(record=True) as warned: client.pull(subscription=subscription_path, return_immediately=True) diff --git a/tests/unit/pubsub_v1/test__gapic.py b/tests/unit/pubsub_v1/test__gapic.py deleted file mode 100644 index adff4bbfc..000000000 --- a/tests/unit/pubsub_v1/test__gapic.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from google.cloud.pubsub_v1 import _gapic - - -class SourceClass(object): - def __init__(self): - self.x = "x" - - def method(self): - return "source class instance method" - - @staticmethod - def static_method(): - return "source class static method" - - @classmethod - def class_method(cls): - return "source class class method" - - @classmethod - def denylisted_method(cls): # pragma: NO COVER - return "source class denylisted method" - - -def test_add_method(): - @_gapic.add_methods(SourceClass, ("denylisted_method",)) - class Foo(object): - def __init__(self): - self.api = SourceClass() - - def method(self): # pragma: NO COVER - return "foo class instance method" - - foo = Foo() - - # Any method that's callable and not denylisted is "inherited". - assert set(["method", "static_method", "class_method"]) <= set(dir(foo)) - assert "denylisted_method" not in dir(foo) - - # Source Class's static and class methods become static methods. - assert type(Foo.__dict__["static_method"]) == staticmethod - assert foo.static_method() == "source class static method" - assert type(Foo.__dict__["class_method"]) == staticmethod - assert foo.class_method() == "source class class method" - - # The decorator changes the behavior of instance methods of the wrapped class. - # method() is called on an instance of the Source Class (stored as an - # attribute on the wrapped class). - assert foo.method() == "source class instance method"