diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml
index 453b540c..773c1dfd 100644
--- a/.github/.OwlBot.lock.yaml
+++ b/.github/.OwlBot.lock.yaml
@@ -13,5 +13,5 @@
# limitations under the License.
docker:
image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest
- digest: sha256:caffe0a9277daeccc4d1de5c9b55ebba0901b57c2f713ec9c876b0d4ec064f61
-# created: 2023-11-08T19:46:45.022803742Z
+ digest: sha256:2f155882785883336b4468d5218db737bb1d10c9cea7cb62219ad16fe248c03c
+# created: 2023-11-29T14:54:29.548172703Z
diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml
index cdec3e9b..a19b27a7 100644
--- a/.github/sync-repo-settings.yaml
+++ b/.github/sync-repo-settings.yaml
@@ -16,13 +16,16 @@ branchProtectionRules:
- 'unit_grpc_gcp-3.9'
- 'unit_grpc_gcp-3.10'
- 'unit_grpc_gcp-3.11'
+ - 'unit_grpc_gcp-3.12'
- 'unit-3.7'
- 'unit-3.8'
- 'unit-3.9'
- 'unit-3.10'
- 'unit-3.11'
+ - 'unit-3.12'
- 'unit_wo_grpc-3.10'
- 'unit_wo_grpc-3.11'
+ - 'unit_wo_grpc-3.12'
- 'cover'
- 'docs'
- 'docfx'
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d2aee5b7..1051da0b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,9 +8,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install nox
diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml
index a505525d..e6a79291 100644
--- a/.github/workflows/mypy.yml
+++ b/.github/workflows/mypy.yml
@@ -8,9 +8,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install nox
diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml
index 0d7789b6..2cfaada3 100644
--- a/.github/workflows/unittest.yml
+++ b/.github/workflows/unittest.yml
@@ -18,6 +18,7 @@ jobs:
- "3.9"
- "3.10"
- "3.11"
+ - "3.12"
exclude:
- option: "_wo_grpc"
python: 3.7
@@ -27,9 +28,9 @@ jobs:
python: 3.9
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install nox
@@ -54,9 +55,9 @@ jobs:
- run-unittests
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install coverage
diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt
index 8957e211..e5c1ffca 100644
--- a/.kokoro/requirements.txt
+++ b/.kokoro/requirements.txt
@@ -93,30 +93,30 @@ colorlog==6.7.0 \
# via
# gcp-docuploader
# nox
-cryptography==41.0.5 \
- --hash=sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf \
- --hash=sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84 \
- --hash=sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e \
- --hash=sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8 \
- --hash=sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7 \
- --hash=sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1 \
- --hash=sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88 \
- --hash=sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86 \
- --hash=sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179 \
- --hash=sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81 \
- --hash=sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20 \
- --hash=sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548 \
- --hash=sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d \
- --hash=sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d \
- --hash=sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5 \
- --hash=sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1 \
- --hash=sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147 \
- --hash=sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936 \
- --hash=sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797 \
- --hash=sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696 \
- --hash=sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72 \
- --hash=sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da \
- --hash=sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723
+cryptography==41.0.6 \
+ --hash=sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596 \
+ --hash=sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c \
+ --hash=sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660 \
+ --hash=sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4 \
+ --hash=sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead \
+ --hash=sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed \
+ --hash=sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3 \
+ --hash=sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7 \
+ --hash=sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09 \
+ --hash=sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c \
+ --hash=sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43 \
+ --hash=sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65 \
+ --hash=sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6 \
+ --hash=sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da \
+ --hash=sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c \
+ --hash=sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b \
+ --hash=sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8 \
+ --hash=sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c \
+ --hash=sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d \
+ --hash=sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9 \
+ --hash=sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86 \
+ --hash=sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36 \
+ --hash=sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae
# via
# gcp-releasetool
# secretstorage
diff --git a/.kokoro/samples/python3.12/common.cfg b/.kokoro/samples/python3.12/common.cfg
new file mode 100644
index 00000000..8a5840a7
--- /dev/null
+++ b/.kokoro/samples/python3.12/common.cfg
@@ -0,0 +1,40 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+ define_artifacts {
+ regex: "**/*sponge_log.xml"
+ }
+}
+
+# Specify which tests to run
+env_vars: {
+ key: "RUN_TESTS_SESSION"
+ value: "py-3.12"
+}
+
+# Declare build specific Cloud project.
+env_vars: {
+ key: "BUILD_SPECIFIC_GCLOUD_PROJECT"
+ value: "python-docs-samples-tests-312"
+}
+
+env_vars: {
+ key: "TRAMPOLINE_BUILD_FILE"
+ value: "github/python-api-core/.kokoro/test-samples.sh"
+}
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+ key: "TRAMPOLINE_IMAGE"
+ value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
+}
+
+# Download secrets for samples
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "python-api-core/.kokoro/trampoline_v2.sh"
\ No newline at end of file
diff --git a/.kokoro/samples/python3.12/continuous.cfg b/.kokoro/samples/python3.12/continuous.cfg
new file mode 100644
index 00000000..a1c8d975
--- /dev/null
+++ b/.kokoro/samples/python3.12/continuous.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+ key: "INSTALL_LIBRARY_FROM_SOURCE"
+ value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.12/periodic-head.cfg b/.kokoro/samples/python3.12/periodic-head.cfg
new file mode 100644
index 00000000..a18c0cfc
--- /dev/null
+++ b/.kokoro/samples/python3.12/periodic-head.cfg
@@ -0,0 +1,11 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+ key: "INSTALL_LIBRARY_FROM_SOURCE"
+ value: "True"
+}
+
+env_vars: {
+ key: "TRAMPOLINE_BUILD_FILE"
+ value: "github/python-api-core/.kokoro/test-samples-against-head.sh"
+}
diff --git a/.kokoro/samples/python3.12/periodic.cfg b/.kokoro/samples/python3.12/periodic.cfg
new file mode 100644
index 00000000..71cd1e59
--- /dev/null
+++ b/.kokoro/samples/python3.12/periodic.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+ key: "INSTALL_LIBRARY_FROM_SOURCE"
+ value: "False"
+}
diff --git a/.kokoro/samples/python3.12/presubmit.cfg b/.kokoro/samples/python3.12/presubmit.cfg
new file mode 100644
index 00000000..a1c8d975
--- /dev/null
+++ b/.kokoro/samples/python3.12/presubmit.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+ key: "INSTALL_LIBRARY_FROM_SOURCE"
+ value: "True"
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf7e3d20..0c1026c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,22 @@
[1]: https://pypi.org/project/google-api-core/#history
+## [2.15.0](https://github.com/googleapis/python-api-core/compare/v2.14.0...v2.15.0) (2023-12-07)
+
+
+### Features
+
+* Add support for Python 3.12 ([#557](https://github.com/googleapis/python-api-core/issues/557)) ([091b4f1](https://github.com/googleapis/python-api-core/commit/091b4f1c7fcc59c3f2a02ee44fd3c30b78423f12))
+* Add type annotations to wrapped grpc calls ([#554](https://github.com/googleapis/python-api-core/issues/554)) ([fc12b40](https://github.com/googleapis/python-api-core/commit/fc12b40bfc6e0c4bb313196e2e3a9c9374ce1c45))
+* Add universe_domain argument to ClientOptions ([3069ef4](https://github.com/googleapis/python-api-core/commit/3069ef4b9123ddb64841cbb7bbb183b53d502e0a))
+* Introduce compatibility with native namespace packages ([#561](https://github.com/googleapis/python-api-core/issues/561)) ([bd82827](https://github.com/googleapis/python-api-core/commit/bd82827108f1eeb6c05cfacf6c044b2afacc18a2))
+
+
+### Bug Fixes
+
+* Fix regression in `bidi` causing `Thread-ConsumeBidirectionalStream caught unexpected exception and will exit` ([#562](https://github.com/googleapis/python-api-core/issues/562)) ([40c8ae0](https://github.com/googleapis/python-api-core/commit/40c8ae0cf1f797e31e106461164e22db4fb2d3d9))
+* Replace deprecated `datetime.datetime.utcnow()` ([#552](https://github.com/googleapis/python-api-core/issues/552)) ([448923a](https://github.com/googleapis/python-api-core/commit/448923acf277a70e8704c949311bf4feaef8cab6)), closes [#540](https://github.com/googleapis/python-api-core/issues/540)
+
## [2.14.0](https://github.com/googleapis/python-api-core/compare/v2.13.1...v2.14.0) (2023-11-09)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 13d7a516..8d1475ce 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -21,7 +21,7 @@ In order to add a feature:
documentation.
- The feature must work fully on the following CPython versions:
- 3.7, 3.8, 3.9, 3.10, and 3.11 on both UNIX and Windows.
+ 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12 on both UNIX and Windows.
- The feature must not add unnecessary dependencies (where
"unnecessary" is of course subjective, but new dependencies should
@@ -71,7 +71,7 @@ We use `nox `__ to instrument our tests.
- To run a single unit test::
- $ nox -s unit-3.11 -- -k
+ $ nox -s unit-3.12 -- -k
.. note::
@@ -202,12 +202,14 @@ We support:
- `Python 3.9`_
- `Python 3.10`_
- `Python 3.11`_
+- `Python 3.12`_
.. _Python 3.7: https://docs.python.org/3.7/
.. _Python 3.8: https://docs.python.org/3.8/
.. _Python 3.9: https://docs.python.org/3.9/
.. _Python 3.10: https://docs.python.org/3.10/
.. _Python 3.11: https://docs.python.org/3.11/
+.. _Python 3.12: https://docs.python.org/3.12/
Supported versions can be found in our ``noxfile.py`` `config`_.
diff --git a/google/__init__.py b/google/__init__.py
deleted file mode 100644
index 9f1d5491..00000000
--- a/google/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright 2016 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Google namespace package."""
-
-try:
- import pkg_resources
-
- pkg_resources.declare_namespace(__name__)
-except ImportError:
- import pkgutil
-
- # See: https://github.com/python/mypy/issues/1422
- __path__ = pkgutil.extend_path(__path__, __name__) # type: ignore
diff --git a/google/api_core/bidi.py b/google/api_core/bidi.py
index 74abc495..78d98b98 100644
--- a/google/api_core/bidi.py
+++ b/google/api_core/bidi.py
@@ -91,8 +91,9 @@ def __init__(self, queue, period=1, initial_request=None):
def _is_active(self):
# Note: there is a possibility that this starts *before* the call
# property is set. So we have to check if self.call is set before
- # seeing if it's active.
- return self.call is not None and self.call.is_active()
+ # seeing if it's active. We need to return True if self.call is None.
+ # See https://github.com/googleapis/python-api-core/issues/560.
+ return self.call is None or self.call.is_active()
def __iter__(self):
if self._initial_request is not None:
diff --git a/google/api_core/client_options.py b/google/api_core/client_options.py
index ee9f28a9..e93f9586 100644
--- a/google/api_core/client_options.py
+++ b/google/api_core/client_options.py
@@ -75,6 +75,11 @@ class ClientOptions(object):
authentication flows. Audience is typically a resource identifier.
If not set, the service endpoint value will be used as a default.
An example of a valid ``api_audience`` is: "https://language.googleapis.com".
+ universe_domain (Optional[str]): The desired universe domain. This must match
+ the one in credentials. If not set, the default universe domain is
+ `googleapis.com`. If both `api_endpoint` and `universe_domain` are set,
+ then `api_endpoint` is used as the service endpoint. If `api_endpoint` is
+ not specified, the format will be `{service}.{universe_domain}`.
Raises:
ValueError: If both ``client_cert_source`` and ``client_encrypted_cert_source``
@@ -91,6 +96,7 @@ def __init__(
scopes=None,
api_key=None,
api_audience=None,
+ universe_domain=None,
):
if client_cert_source and client_encrypted_cert_source:
raise ValueError(
@@ -106,6 +112,7 @@ def __init__(
self.scopes = scopes
self.api_key = api_key
self.api_audience = api_audience
+ self.universe_domain = universe_domain
def __repr__(self):
return "ClientOptions: " + repr(self.__dict__)
diff --git a/google/api_core/datetime_helpers.py b/google/api_core/datetime_helpers.py
index c584c8cd..c3792300 100644
--- a/google/api_core/datetime_helpers.py
+++ b/google/api_core/datetime_helpers.py
@@ -42,7 +42,7 @@
def utcnow():
"""A :meth:`datetime.datetime.utcnow()` alias to allow mocking in tests."""
- return datetime.datetime.utcnow()
+ return datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
def to_milliseconds(value):
diff --git a/google/api_core/grpc_helpers.py b/google/api_core/grpc_helpers.py
index f52e180a..793c884d 100644
--- a/google/api_core/grpc_helpers.py
+++ b/google/api_core/grpc_helpers.py
@@ -13,6 +13,7 @@
# limitations under the License.
"""Helpers for :mod:`grpc`."""
+from typing import Generic, TypeVar, Iterator
import collections
import functools
@@ -54,6 +55,9 @@
_LOGGER = logging.getLogger(__name__)
+# denotes the proto response type for grpc calls
+P = TypeVar("P")
+
def _patch_callable_name(callable_):
"""Fix-up gRPC callable attributes.
@@ -79,7 +83,7 @@ def error_remapped_callable(*args, **kwargs):
return error_remapped_callable
-class _StreamingResponseIterator(grpc.Call):
+class _StreamingResponseIterator(Generic[P], grpc.Call):
def __init__(self, wrapped, prefetch_first_result=True):
self._wrapped = wrapped
@@ -97,11 +101,11 @@ def __init__(self, wrapped, prefetch_first_result=True):
# ignore stop iteration at this time. This should be handled outside of retry.
pass
- def __iter__(self):
+ def __iter__(self) -> Iterator[P]:
"""This iterator is also an iterable that returns itself."""
return self
- def __next__(self):
+ def __next__(self) -> P:
"""Get the next response from the stream.
Returns:
@@ -144,6 +148,10 @@ def trailing_metadata(self):
return self._wrapped.trailing_metadata()
+# public type alias denoting the return type of streaming gapic calls
+GrpcStream = _StreamingResponseIterator[P]
+
+
def _wrap_stream_errors(callable_):
"""Wrap errors for Unary-Stream and Stream-Stream gRPC callables.
diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py
index d1f69d98..5685e6f8 100644
--- a/google/api_core/grpc_helpers_async.py
+++ b/google/api_core/grpc_helpers_async.py
@@ -21,11 +21,15 @@
import asyncio
import functools
+from typing import Generic, Iterator, AsyncGenerator, TypeVar
+
import grpc
from grpc import aio
from google.api_core import exceptions, grpc_helpers
+# denotes the proto response type for grpc calls
+P = TypeVar("P")
# NOTE(lidiz) Alternatively, we can hack "__getattribute__" to perform
# automatic patching for us. But that means the overhead of creating an
@@ -75,8 +79,8 @@ async def wait_for_connection(self):
raise exceptions.from_grpc_error(rpc_error) from rpc_error
-class _WrappedUnaryResponseMixin(_WrappedCall):
- def __await__(self):
+class _WrappedUnaryResponseMixin(Generic[P], _WrappedCall):
+ def __await__(self) -> Iterator[P]:
try:
response = yield from self._call.__await__()
return response
@@ -84,17 +88,17 @@ def __await__(self):
raise exceptions.from_grpc_error(rpc_error) from rpc_error
-class _WrappedStreamResponseMixin(_WrappedCall):
+class _WrappedStreamResponseMixin(Generic[P], _WrappedCall):
def __init__(self):
self._wrapped_async_generator = None
- async def read(self):
+ async def read(self) -> P:
try:
return await self._call.read()
except grpc.RpcError as rpc_error:
raise exceptions.from_grpc_error(rpc_error) from rpc_error
- async def _wrapped_aiter(self):
+ async def _wrapped_aiter(self) -> AsyncGenerator[P, None]:
try:
# NOTE(lidiz) coverage doesn't understand the exception raised from
# __anext__ method. It is covered by test case:
@@ -104,7 +108,7 @@ async def _wrapped_aiter(self):
except grpc.RpcError as rpc_error:
raise exceptions.from_grpc_error(rpc_error) from rpc_error
- def __aiter__(self):
+ def __aiter__(self) -> AsyncGenerator[P, None]:
if not self._wrapped_async_generator:
self._wrapped_async_generator = self._wrapped_aiter()
return self._wrapped_async_generator
@@ -127,26 +131,32 @@ async def done_writing(self):
# NOTE(lidiz) Implementing each individual class separately, so we don't
# expose any API that should not be seen. E.g., __aiter__ in unary-unary
# RPC, or __await__ in stream-stream RPC.
-class _WrappedUnaryUnaryCall(_WrappedUnaryResponseMixin, aio.UnaryUnaryCall):
+class _WrappedUnaryUnaryCall(_WrappedUnaryResponseMixin[P], aio.UnaryUnaryCall):
"""Wrapped UnaryUnaryCall to map exceptions."""
-class _WrappedUnaryStreamCall(_WrappedStreamResponseMixin, aio.UnaryStreamCall):
+class _WrappedUnaryStreamCall(_WrappedStreamResponseMixin[P], aio.UnaryStreamCall):
"""Wrapped UnaryStreamCall to map exceptions."""
class _WrappedStreamUnaryCall(
- _WrappedUnaryResponseMixin, _WrappedStreamRequestMixin, aio.StreamUnaryCall
+ _WrappedUnaryResponseMixin[P], _WrappedStreamRequestMixin, aio.StreamUnaryCall
):
"""Wrapped StreamUnaryCall to map exceptions."""
class _WrappedStreamStreamCall(
- _WrappedStreamRequestMixin, _WrappedStreamResponseMixin, aio.StreamStreamCall
+ _WrappedStreamRequestMixin, _WrappedStreamResponseMixin[P], aio.StreamStreamCall
):
"""Wrapped StreamStreamCall to map exceptions."""
+# public type alias denoting the return type of async streaming gapic calls
+GrpcAsyncStream = _WrappedStreamResponseMixin[P]
+# public type alias denoting the return type of unary gapic calls
+AwaitableGrpcCall = _WrappedUnaryResponseMixin[P]
+
+
def _wrap_unary_errors(callable_):
"""Map errors for Unary-Unary async callables."""
grpc_helpers._patch_callable_name(callable_)
diff --git a/google/api_core/version.py b/google/api_core/version.py
index ba8b4e8a..a8381fff 100644
--- a/google/api_core/version.py
+++ b/google/api_core/version.py
@@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = "2.14.0"
+__version__ = "2.15.0"
diff --git a/noxfile.py b/noxfile.py
index 82ea2669..2b668e7b 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -143,13 +143,13 @@ def default(session, install_grpc=True):
session.run(*pytest_args)
-@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11"])
+@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"])
def unit(session):
"""Run the unit test suite."""
default(session)
-@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11"])
+@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"])
def unit_grpc_gcp(session):
"""Run the unit test suite with grpcio-gcp installed."""
constraints_path = str(
@@ -163,7 +163,7 @@ def unit_grpc_gcp(session):
default(session)
-@nox.session(python=["3.8", "3.10", "3.11"])
+@nox.session(python=["3.8", "3.10", "3.11", "3.12"])
def unit_wo_grpc(session):
"""Run the unit test suite w/o grpcio installed"""
default(session, install_grpc=False)
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..66f72e41
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,21 @@
+[pytest]
+filterwarnings =
+ # treat all warnings as errors
+ error
+ # Remove once https://github.com/pytest-dev/pytest-cov/issues/621 is fixed
+ ignore:.*The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning
+ # Remove once https://github.com/protocolbuffers/protobuf/issues/12186 is fixed
+ ignore:.*custom tp_new.*in Python 3.14:DeprecationWarning
+ # Remove once support for python 3.7 is dropped
+ # This warning only appears when using python 3.7
+ ignore:.*Using or importing the ABCs from.*collections:DeprecationWarning
+ # Remove once support for grpcio-gcp is deprecated
+ # See https://github.com/googleapis/python-api-core/blob/42e8b6e6f426cab749b34906529e8aaf3f133d75/google/api_core/grpc_helpers.py#L39-L45
+ ignore:.*Support for grpcio-gcp is deprecated:DeprecationWarning
+ # Remove once https://github.com/googleapis/python-api-common-protos/pull/187/files is merged
+ ignore:.*pkg_resources.declare_namespace:DeprecationWarning
+ ignore:.*pkg_resources is deprecated as an API:DeprecationWarning
+ # Remove once release PR https://github.com/googleapis/proto-plus-python/pull/391 is merged
+ ignore:datetime.datetime.utcfromtimestamp\(\) is deprecated:DeprecationWarning:proto.datetime_helpers
+ # Remove once https://github.com/grpc/grpc/issues/35086 is fixed
+ ignore:There is no current event loop:DeprecationWarning
diff --git a/setup.py b/setup.py
index d4639a90..47a3c203 100644
--- a/setup.py
+++ b/setup.py
@@ -63,15 +63,11 @@
# Only include packages under the 'google' namespace. Do not include tests,
# benchmarks, etc.
packages = [
- package for package in setuptools.find_packages() if package.startswith("google")
+ package
+ for package in setuptools.find_namespace_packages()
+ if package.startswith("google")
]
-# Determine which namespaces are needed.
-namespaces = ["google"]
-if "google.cloud" in packages:
- namespaces.append("google.cloud")
-
-
setuptools.setup(
name=name,
version=version,
@@ -92,12 +88,12 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"Topic :: Internet",
],
platforms="Posix; MacOS X; Windows",
packages=packages,
- namespace_packages=namespaces,
install_requires=dependencies,
extras_require=extras,
python_requires=">=3.7",
diff --git a/testing/constraints-3.12.txt b/testing/constraints-3.12.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py
index 95242f6b..67c9b335 100644
--- a/tests/asyncio/test_grpc_helpers_async.py
+++ b/tests/asyncio/test_grpc_helpers_async.py
@@ -266,6 +266,28 @@ def test_wrap_errors_non_streaming(wrap_unary_errors):
wrap_unary_errors.assert_called_once_with(callable_)
+def test_grpc_async_stream():
+ """
+ GrpcAsyncStream type should be both an AsyncIterator and a grpc.aio.Call.
+ """
+ instance = grpc_helpers_async.GrpcAsyncStream[int]()
+ assert isinstance(instance, grpc.aio.Call)
+ # should implement __aiter__ and __anext__
+ assert hasattr(instance, "__aiter__")
+ it = instance.__aiter__()
+ assert hasattr(it, "__anext__")
+
+
+def test_awaitable_grpc_call():
+ """
+ AwaitableGrpcCall type should be an Awaitable and a grpc.aio.Call.
+ """
+ instance = grpc_helpers_async.AwaitableGrpcCall[int]()
+ assert isinstance(instance, grpc.aio.Call)
+ # should implement __await__
+ assert hasattr(instance, "__await__")
+
+
@mock.patch("google.api_core.grpc_helpers_async._wrap_stream_errors")
def test_wrap_errors_streaming(wrap_stream_errors):
callable_ = mock.create_autospec(aio.UnaryStreamMultiCallable)
diff --git a/tests/asyncio/test_retry_async.py b/tests/asyncio/test_retry_async.py
index 14807eb5..16f5c3db 100644
--- a/tests/asyncio/test_retry_async.py
+++ b/tests/asyncio/test_retry_async.py
@@ -280,7 +280,6 @@ async def test___call___and_execute_success(self, sleep):
@mock.patch("asyncio.sleep", autospec=True)
@pytest.mark.asyncio
async def test___call___and_execute_retry(self, sleep, uniform):
-
on_error = mock.Mock(spec=["__call__"], side_effect=[None])
retry_ = retry_async.AsyncRetry(
predicate=retry_async.if_exception_type(ValueError)
@@ -305,7 +304,6 @@ async def test___call___and_execute_retry(self, sleep, uniform):
@mock.patch("asyncio.sleep", autospec=True)
@pytest.mark.asyncio
async def test___call___and_execute_retry_hitting_deadline(self, sleep, uniform):
-
on_error = mock.Mock(spec=["__call__"], side_effect=[None] * 10)
retry_ = retry_async.AsyncRetry(
predicate=retry_async.if_exception_type(ValueError),
@@ -315,7 +313,7 @@ async def test___call___and_execute_retry_hitting_deadline(self, sleep, uniform)
deadline=9.9,
)
- utcnow = datetime.datetime.utcnow()
+ utcnow = datetime.datetime.now(tz=datetime.timezone.utc)
utcnow_patcher = mock.patch(
"google.api_core.datetime_helpers.utcnow", return_value=utcnow
)
diff --git a/tests/unit/test_client_options.py b/tests/unit/test_client_options.py
index 336ceeab..396d6627 100644
--- a/tests/unit/test_client_options.py
+++ b/tests/unit/test_client_options.py
@@ -38,6 +38,7 @@ def test_constructor():
"https://www.googleapis.com/auth/cloud-platform.read-only",
],
api_audience="foo2.googleapis.com",
+ universe_domain="googleapis.com",
)
assert options.api_endpoint == "foo.googleapis.com"
@@ -49,6 +50,7 @@ def test_constructor():
"https://www.googleapis.com/auth/cloud-platform.read-only",
]
assert options.api_audience == "foo2.googleapis.com"
+ assert options.universe_domain == "googleapis.com"
def test_constructor_with_encrypted_cert_source():
@@ -110,6 +112,7 @@ def test_from_dict():
options = client_options.from_dict(
{
"api_endpoint": "foo.googleapis.com",
+ "universe_domain": "googleapis.com",
"client_cert_source": get_client_cert,
"quota_project_id": "quote-proj",
"credentials_file": "path/to/credentials.json",
@@ -122,6 +125,7 @@ def test_from_dict():
)
assert options.api_endpoint == "foo.googleapis.com"
+ assert options.universe_domain == "googleapis.com"
assert options.client_cert_source() == (b"cert", b"key")
assert options.quota_project_id == "quote-proj"
assert options.credentials_file == "path/to/credentials.json"
@@ -148,6 +152,7 @@ def test_repr():
expected_keys = set(
[
"api_endpoint",
+ "universe_domain",
"client_cert_source",
"client_encrypted_cert_source",
"quota_project_id",
diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py
index 4eccbcaa..58a6a329 100644
--- a/tests/unit/test_grpc_helpers.py
+++ b/tests/unit/test_grpc_helpers.py
@@ -195,6 +195,23 @@ def test_trailing_metadata(self):
wrapped.trailing_metadata.assert_called_once_with()
+class TestGrpcStream(Test_StreamingResponseIterator):
+ @staticmethod
+ def _make_one(wrapped, **kw):
+ return grpc_helpers.GrpcStream(wrapped, **kw)
+
+ def test_grpc_stream_attributes(self):
+ """
+ Should be both a grpc.Call and an iterable
+ """
+ call = self._make_one(None)
+ assert isinstance(call, grpc.Call)
+ # should implement __iter__
+ assert hasattr(call, "__iter__")
+ it = call.__iter__()
+ assert hasattr(it, "__next__")
+
+
def test_wrap_stream_okay():
expected_responses = [1, 2, 3]
callable_ = mock.Mock(spec=["__call__"], return_value=iter(expected_responses))
diff --git a/tests/unit/test_iam.py b/tests/unit/test_iam.py
index fbd242e5..3de15288 100644
--- a/tests/unit/test_iam.py
+++ b/tests/unit/test_iam.py
@@ -167,14 +167,15 @@ def test_owners_getter(self):
assert policy.owners == expected
def test_owners_setter(self):
- import warnings
from google.api_core.iam import OWNER_ROLE
MEMBER = "user:phred@example.com"
expected = set([MEMBER])
policy = self._make_one()
- with warnings.catch_warnings(record=True) as warned:
+ with pytest.warns(
+ DeprecationWarning, match="Assigning to 'owners' is deprecated."
+ ) as warned:
policy.owners = [MEMBER]
(warning,) = warned
@@ -191,14 +192,15 @@ def test_editors_getter(self):
assert policy.editors == expected
def test_editors_setter(self):
- import warnings
from google.api_core.iam import EDITOR_ROLE
MEMBER = "user:phred@example.com"
expected = set([MEMBER])
policy = self._make_one()
- with warnings.catch_warnings(record=True) as warned:
+ with pytest.warns(
+ DeprecationWarning, match="Assigning to 'editors' is deprecated."
+ ) as warned:
policy.editors = [MEMBER]
(warning,) = warned
@@ -215,14 +217,15 @@ def test_viewers_getter(self):
assert policy.viewers == expected
def test_viewers_setter(self):
- import warnings
from google.api_core.iam import VIEWER_ROLE
MEMBER = "user:phred@example.com"
expected = set([MEMBER])
policy = self._make_one()
- with warnings.catch_warnings(record=True) as warned:
+ with pytest.warns(
+ DeprecationWarning, match="Assigning to 'viewers' is deprecated."
+ ) as warned:
policy.viewers = [MEMBER]
(warning,) = warned
@@ -337,12 +340,13 @@ def test_to_api_repr_binding_wo_members(self):
assert policy.to_api_repr() == {}
def test_to_api_repr_binding_w_duplicates(self):
- import warnings
from google.api_core.iam import OWNER_ROLE
OWNER = "group:cloud-logs@google.com"
policy = self._make_one()
- with warnings.catch_warnings(record=True):
+ with pytest.warns(
+ DeprecationWarning, match="Assigning to 'owners' is deprecated."
+ ):
policy.owners = [OWNER, OWNER]
assert policy.to_api_repr() == {
"bindings": [{"role": OWNER_ROLE, "members": [OWNER]}]
diff --git a/tests/unit/test_packaging.py b/tests/unit/test_packaging.py
new file mode 100644
index 00000000..8100a496
--- /dev/null
+++ b/tests/unit/test_packaging.py
@@ -0,0 +1,28 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import subprocess
+import sys
+
+
+def test_namespace_package_compat(tmp_path):
+ # The ``google`` namespace package should not be masked
+ # by the presence of ``google-api-core``.
+ google = tmp_path / "google"
+ google.mkdir()
+ google.joinpath("othermod.py").write_text("")
+ env = dict(os.environ, PYTHONPATH=str(tmp_path))
+ cmd = [sys.executable, "-m", "google.othermod"]
+ subprocess.check_call(cmd, env=env)
diff --git a/tests/unit/test_retry.py b/tests/unit/test_retry.py
index d770f7a2..2faf77c9 100644
--- a/tests/unit/test_retry.py
+++ b/tests/unit/test_retry.py
@@ -361,7 +361,6 @@ def test___call___and_execute_success(self, sleep):
@mock.patch("random.uniform", autospec=True, side_effect=lambda m, n: n)
@mock.patch("time.sleep", autospec=True)
def test___call___and_execute_retry(self, sleep, uniform):
-
on_error = mock.Mock(spec=["__call__"], side_effect=[None])
retry_ = retry.Retry(predicate=retry.if_exception_type(ValueError))
@@ -383,7 +382,6 @@ def test___call___and_execute_retry(self, sleep, uniform):
@mock.patch("random.uniform", autospec=True, side_effect=lambda m, n: n)
@mock.patch("time.sleep", autospec=True)
def test___call___and_execute_retry_hitting_deadline(self, sleep, uniform):
-
on_error = mock.Mock(spec=["__call__"], side_effect=[None] * 10)
retry_ = retry.Retry(
predicate=retry.if_exception_type(ValueError),
@@ -393,7 +391,7 @@ def test___call___and_execute_retry_hitting_deadline(self, sleep, uniform):
deadline=30.9,
)
- utcnow = datetime.datetime.utcnow()
+ utcnow = datetime.datetime.now(tz=datetime.timezone.utc)
utcnow_patcher = mock.patch(
"google.api_core.datetime_helpers.utcnow", return_value=utcnow
)
diff --git a/tests/unit/test_timeout.py b/tests/unit/test_timeout.py
index a83a2ecb..0bcf07f0 100644
--- a/tests/unit/test_timeout.py
+++ b/tests/unit/test_timeout.py
@@ -58,10 +58,10 @@ def test___str__(self):
def test_apply(self):
target = mock.Mock(spec=["__call__", "__name__"], __name__="target")
- datetime.datetime.utcnow()
+ datetime.datetime.now(tz=datetime.timezone.utc)
datetime.timedelta(seconds=1)
- now = datetime.datetime.utcnow()
+ now = datetime.datetime.now(tz=datetime.timezone.utc)
times = [
now,
@@ -92,10 +92,10 @@ def _clock():
def test_apply_no_timeout(self):
target = mock.Mock(spec=["__call__", "__name__"], __name__="target")
- datetime.datetime.utcnow()
+ datetime.datetime.now(tz=datetime.timezone.utc)
datetime.timedelta(seconds=1)
- now = datetime.datetime.utcnow()
+ now = datetime.datetime.now(tz=datetime.timezone.utc)
times = [
now,