Repository: googleapis/python-api-core

Issue:
Backward compatible import of retry_async does not work in 2.16.0
The code aimed to maintain backwards compatibity when importing `retry_async` did not work when the .


#### Environment details

  - OS type and version: Dockerised Debian Bookwork
  - Python version: `python --version`: 3.8.18
  - pip version: `pip --version`: 23.3.2
  - `google-api-core` version: `pip show google-api-core`

```
Name: google-api-core
Version: 2.16.0
Summary: Google API client core library
Home-page: https://github.com/googleapis/python-api-core
Author: Google LLC
Author-email: googleapis-packages@google.com
License: Apache 2.0
Location: /usr/local/lib/python3.8/site-packages
Requires: google-auth, googleapis-common-protos, protobuf, requests
Required-by: google-ads, google-analytics-admin, google-api-python-client, google-cloud-aiplatform, google-cloud-appengine-logging, google-cloud-automl, google-cloud-batch, google-cloud-bigquery, google-cloud-bigquery-datatransfer, google-cloud-bigquery-storage, google-cloud-bigtable, google-cloud-build, google-cloud-compute, google-cloud-container, google-cloud-core, google-cloud-datacatalog, google-cloud-dataflow-client, google-cloud-dataform, google-cloud-dataplex, google-cloud-dataproc, google-cloud-dataproc-metastore, google-cloud-dlp, google-cloud-kms, google-cloud-language, google-cloud-logging, google-cloud-memcache, google-cloud-monitoring, google-cloud-orchestration-airflow, google-cloud-os-login, google-cloud-pubsub, google-cloud-redis, google-cloud-resource-manager, google-cloud-run, google-cloud-secret-manager, google-cloud-spanner, google-cloud-speech, google-cloud-storage, google-cloud-storage-transfer, google-cloud-tasks, google-cloud-texttospeech, google-cloud-translate, google-cloud-videointelligence, google-cloud-vision, google-cloud-workflows, pandas-gbq, sqlalchemy-bigquery
```

#### Steps to reproduce

  1. Run `pip insall google-api-core==2.15.0`
  2. Enter python repl with `python`
  3. Run `from google.api_core.retry_async import AsyncRetry` -> this nicely works
  4. Run `from google.api_core.retry import AsyncRetry` -> this does not work (as expected)

```
>>> from google.api_core.retry import AsyncRetry
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'AsyncRetry' from 'google.api_core.retry' (/usr/local/lib/python3.8/site-packages/google/api_core/retry.py)
```

5. Run `pip insall google-api-core==2.16.0`
6. Enter python repl with `python`
7. Run `from google.api_core.retry_async import AsyncRetry` -> this does not work (but should)

```
>>> from google.api_core.retry_async import AsyncRetry
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'google.api_core.retry_async'
>>> 
```
8. Run `from google.api_core.retry import AsyncRetry` -> this works (as expected)

----

There was an attempt in #495 to make it work:

In: google/api_core/__init__.py

```python
# for backwards compatibility, expose async unary retries as google.api_core.retry_async
from .retry import retry_unary_async as retry_async  # noqa: F401
```

This is aimed to make `from google.api_core.retry_async import AsyncRetry` works, but importing a module into another modules `__init__` does not work the way it is supposed to work.

While there was attempt to even test it in #577, it did not test the right imports:

```python
def test_legacy_imports_retry_unary_async():
    # TODO: Delete this test when when we revert these imports on the
    #       next major version release
    #       (https://github.com/googleapis/python-api-core/issues/576)
    from google.api_core import retry_async  # noqa: F401
```

The ` from google.api_core import retry_async` works, fine, but `from google.api_core.retry_async import AsyncRetry` still raises the `No module named 'google.api_core.retry_async` - bycause of the way how python resolves `from`.

I guess good solution will be to add back the `retry_async` as a (mostly empty) submodule and import all the needed classess from the `retry_unary_async` package

#### Code example

```python
from google.api_core.retry_async import AsyncRetry
from google.api_core.retry import AsyncRetry
```

#### Stack trace
```
>>> from google.api_core.retry_async import AsyncRetry
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'google.api_core.retry_async'
>>> 
```

Making sure to follow these steps will guarantee the quickest resolution possible.

Thanks!


Discussion:

Patch:
diff --git a/docs/retry.rst b/docs/retry.rst
index 97a7f2c..6e165f5 100644
--- a/docs/retry.rst
+++ b/docs/retry.rst
@@ -10,4 +10,5 @@ Retry in AsyncIO
 
 .. automodule:: google.api_core.retry_async
   :members:
+  :noindex:
   :show-inheritance:
diff --git a/google/api_core/__init__.py b/google/api_core/__init__.py
index 89ce751..b80ea37 100644
--- a/google/api_core/__init__.py
+++ b/google/api_core/__init__.py
@@ -20,6 +20,3 @@ This package contains common code and utilities used by Google client libraries.
 from google.api_core import version as api_core_version
 
 __version__ = api_core_version.__version__
-
-# for backwards compatibility, expose async unary retries as google.api_core.retry_async
-from .retry import retry_unary_async as retry_async  # noqa: F401
diff --git a/google/api_core/retry_async.py b/google/api_core/retry_async.py
new file mode 100644
index 0000000..90a2d5a
--- /dev/null
+++ b/google/api_core/retry_async.py
@@ -0,0 +1,34 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# The following imports are for backwards compatibility with https://github.com/googleapis/python-api-core/blob/4d7d2edee2c108d43deb151e6e0fdceb56b73275/google/api_core/retry_async.py
+#
+# TODO: Revert these imports on the next major version release (https://github.com/googleapis/python-api-core/issues/576)
+from google.api_core import datetime_helpers  # noqa: F401
+from google.api_core import exceptions  # noqa: F401
+from google.api_core.retry import exponential_sleep_generator  # noqa: F401
+from google.api_core.retry import if_exception_type  # noqa: F401
+from google.api_core.retry import if_transient_error  # noqa: F401
+from google.api_core.retry.retry_unary_async import AsyncRetry
+from google.api_core.retry.retry_unary_async import retry_target
+
+__all__ = (
+    "AsyncRetry",
+    "datetime_helpers",
+    "exceptions",
+    "exponential_sleep_generator",
+    "if_exception_type",
+    "if_transient_error",
+    "retry_target",
+)


Test patch:
diff --git a/tests/unit/retry/test_retry_imports.py b/tests/unit/retry/test_retry_imports.py
index 5d035be..597909f 100644
--- a/tests/unit/retry/test_retry_imports.py
+++ b/tests/unit/retry/test_retry_imports.py
@@ -27,3 +27,7 @@ def test_legacy_imports_retry_unary_async():
     #       next major version release
     #       (https://github.com/googleapis/python-api-core/issues/576)
     from google.api_core import retry_async  # noqa: F401
+
+    # See https://github.com/googleapis/python-api-core/issues/586
+    # for context on why we need to test this import this explicitly.
+    from google.api_core.retry_async import AsyncRetry  # noqa: F401