Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Breaking Changes
----------------

- The ``RequestsTransport`` object has been refactored to separate it from
configuration which controls request retries. A new ``RetryConfig`` object is
introduced and provided as ``client.retry_config`` on all client types. The
interface for controlling these configurations has been updated.
(:pr:`NUMBER`)

- The ``transport_class`` attribute has been removed from client classes.

- Clients now accept ``transport``, an instance of ``RequestsTransport``, and
``retry_config``, an instance of ``RetryConfig``, instead of
``transport_params``.

- Users seeking to customize the retry backoff, sleep maximum, and max
retries should now use ``retry_config``, as these are no longer controlled
through ``transport``.

- The capabilities of the ``RequestsTransport.tune()`` context manager have
been divided into ``RequestsTransport.tune()`` and ``RetryConfig.tune()``.

- The retry configuration is exposed to retry checks as an attribute of the
``RequestCallerInfo``, which is provided on the ``RetryContext``. As a
result, checks can examine the configuration.
111 changes: 111 additions & 0 deletions docs/upgrading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,117 @@ Change:
auth_client.oauth2_start_flow(requested_scopes=globus_sdk.TransferClient.scopes.all)
authorize_url = auth_client.oauth2_get_authorize_url()

Customizing the Transport Has Changed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In version 3, SDK users could customize the ``RequestsTransport`` object
contained within a client in two ways.
One was to customize a client class by setting the ``transport_class`` class
attribute, and the other was to pass ``transport_params`` to the client
initializer.

In version 4, these mechanisms have been replaced with support for passing a
``RequestsTransport`` object directly to the initializer.

For users who are customizing the parameters to the transport class, they
should now explicitly instantiate the transport object:

.. code-block:: python

# globus-sdk v3
import globus_sdk

client = globus_sdk.GroupsClient(transport_params={"http_timeout": 120.0})

# globus-sdk v4
import globus_sdk
from globus_sdk.transport import RequestsTransport

client = globus_sdk.GroupsClient(transport=RequestsTransport(http_timeout=120.0))

or use the ``tune()`` context manager:

.. code-block:: python

# globus-sdk v4
import globus_sdk

client = globus_sdk.GroupsClient()
with client.transport.tune(http_timeout=120.0):
my_groups = client.get_my_groups()

Retry Check Configuration Moved to ``retry_config``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In Globus SDK v3, a client's ``transport`` contained all of its retry
behaviors, including the checks which are run on each request, the
configuration of those checks, and the sleep and backoff behaviors.

Under v4, the configuration of checks has been split off into a separate
attribute of the client, ``retry_config``.

These changes primarily impact users who were using a custom
``RequestsTransport`` class, and should simplify their usage.

For example, in order to treat only 502s as retriable transient errors, users
previously had a custom transport type.
This could then be configured on a custom client class:

.. code-block:: python

# globus-sdk v3
import globus_sdk
from globus_sdk.transport import RequestsTransport


class MyTransport(RequestsTransport):
TRANSIENT_ERROR_STATUS_CODES = (502,)


class MyClientClass(globus_sdk.GroupsClient):
transport_class = MyTransport


client = MyClientClass()

Under SDK v4, in order to customize the same information, users can simply
create a client and then modify the attributes of the ``retry_config`` object:

.. code-block:: python

# globus-sdk v4
import globus_sdk

client = globus_sdk.GroupsClient()
client.retry_config.transient_error_status_codes = (502,)

Similar to the ``tune()`` context manager of ``RequestsTransport``, there is
also a ``tune()`` context manager for the retry configuration. ``tune()``
supports the ``max_sleep``, ``max_retries``, and ``backoff`` configurations,
which users of ``RequestsTransport.tune()`` may already recognize.
For example, users can suppress retries:

.. code-block:: python

# globus-sdk v4
import globus_sdk

client = globus_sdk.GroupsClient()
with client.retry_config.tune(max_retries=1):
my_groups = client.get_my_groups()

A ``retry_config`` can also be passed to clients on initialization:

.. code-block:: python

# globus-sdk v4
import globus_sdk
from globus_sdk.transport import RetryConfig

client = globus_sdk.GroupsClient(retry_config=RetryConfig(max_retries=2))
my_groups = client.get_my_groups()


From 1.x or 2.x to 3.0
-----------------------

Expand Down
36 changes: 25 additions & 11 deletions src/globus_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from globus_sdk.paging import PaginatorTable
from globus_sdk.response import GlobusHTTPResponse
from globus_sdk.scopes import Scope, ScopeCollection
from globus_sdk.transport import RequestCallerInfo, RequestsTransport
from globus_sdk.transport import RequestCallerInfo, RequestsTransport, RetryConfig
from globus_sdk.transport.default_retry_checks import DEFAULT_RETRY_CHECKS

if sys.version_info >= (3, 10):
from typing import TypeAlias
Expand Down Expand Up @@ -48,9 +49,11 @@ class BaseClient:
intelligently by default. Set it when inheriting from BaseClient or
communicating through a proxy. This value takes precedence over the class
attribute of the same name.
:param transport_params: Options to pass to the transport for this client

All other parameters are for internal use and should be ignored.
:param transport: A :class:`RequestsTransport` object for sending and
retrying requests. By default, one will be constructed by the client.
:param retry_config: A :class:`RetryConfig` object with parameters to
control request retry behavior. By default, one will be constructed by
the client.
"""

# service name is used to lookup a service URL from config
Expand All @@ -65,9 +68,6 @@ class BaseClient:
#: this can be set in subclasses, but must always be a subclass of GlobusError
error_class: type[exc.GlobusAPIError] = exc.GlobusAPIError

#: the type of Transport which will be used, defaults to ``RequestsTransport``
transport_class: type[RequestsTransport] = RequestsTransport

#: the scopes for this client may be present as a ``ScopeCollection``
scopes: ScopeCollection | None = None

Expand All @@ -80,7 +80,8 @@ def __init__(
app_scopes: list[Scope] | None = None,
authorizer: GlobusAuthorizer | None = None,
app_name: str | None = None,
transport_params: dict[str, t.Any] | None = None,
transport: RequestsTransport | None = None,
retry_config: RetryConfig | None = None,
) -> None:
# check for input parameter conflicts
if app_scopes and not app:
Expand All @@ -107,7 +108,10 @@ def __init__(
# resolve the base_url for the client (see docstring for resolution precedence)
self.base_url = self._resolve_base_url(base_url, self.environment)

self.transport = self.transport_class(**(transport_params or {}))
self.retry_config: RetryConfig = retry_config or RetryConfig()
self._register_standard_retry_checks(self.retry_config)

self.transport = transport if transport is not None else RequestsTransport()
log.debug(f"initialized transport of type {type(self.transport)}")

# setup paginated methods
Expand Down Expand Up @@ -139,6 +143,14 @@ def default_scope_requirements(self) -> list[Scope]:
"""
raise NotImplementedError

def _register_standard_retry_checks(self, retry_config: RetryConfig) -> None:
"""
Setup the standard checks for this client.

This is called during init and may be overridden by subclasses.
"""
retry_config.checks.register_many_checks(DEFAULT_RETRY_CHECKS)

@classmethod
def _resolve_base_url(cls, init_base_url: str | None, environment: str) -> str:
"""
Expand Down Expand Up @@ -496,8 +508,10 @@ def request(
else:
authorizer = None

# create caller info with the authorizer
caller_info = RequestCallerInfo(authorizer=authorizer)
# capture info about this client as the caller to pass to the transport
caller_info = RequestCallerInfo(
retry_config=self.retry_config, authorizer=authorizer
)

# make the request
log.debug("request will hit URL: %s", url)
Expand Down
7 changes: 5 additions & 2 deletions src/globus_sdk/services/auth/client/base_login_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from globus_sdk.authorizers import GlobusAuthorizer, NullAuthorizer
from globus_sdk.response import GlobusHTTPResponse
from globus_sdk.scopes import AuthScopes, Scope
from globus_sdk.transport import RequestsTransport, RetryConfig

from .._common import get_jwk_data, pem_decode_jwk_data
from ..errors import AuthAPIError
Expand Down Expand Up @@ -52,14 +53,16 @@ def __init__(
base_url: str | None = None,
authorizer: GlobusAuthorizer | None = None,
app_name: str | None = None,
transport_params: dict[str, t.Any] | None = None,
transport: RequestsTransport | None = None,
retry_config: RetryConfig | None = None,
) -> None:
super().__init__(
environment=environment,
base_url=base_url,
authorizer=authorizer,
app_name=app_name,
transport_params=transport_params,
transport=transport,
retry_config=retry_config,
)
self.client_id: str | None = str(client_id) if client_id is not None else None
# an AuthClient may contain a GlobusOAuth2FlowManager in order to
Expand Down
7 changes: 5 additions & 2 deletions src/globus_sdk/services/auth/client/confidential_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from globus_sdk.authorizers import BasicAuthorizer
from globus_sdk.response import GlobusHTTPResponse
from globus_sdk.scopes import Scope, ScopeParser
from globus_sdk.transport import RequestsTransport, RetryConfig

from ..flow_managers import GlobusAuthorizationCodeFlowManager
from ..response import OAuthClientCredentialsResponse, OAuthDependentTokenResponse
Expand Down Expand Up @@ -47,15 +48,17 @@ def __init__(
environment: str | None = None,
base_url: str | None = None,
app_name: str | None = None,
transport_params: dict[str, t.Any] | None = None,
transport: RequestsTransport | None = None,
retry_config: RetryConfig | None = None,
) -> None:
super().__init__(
client_id=client_id,
authorizer=BasicAuthorizer(str(client_id), client_secret),
environment=environment,
base_url=base_url,
app_name=app_name,
transport_params=transport_params,
transport=transport,
retry_config=retry_config,
)

def oauth2_client_credentials_tokens(
Expand Down
7 changes: 5 additions & 2 deletions src/globus_sdk/services/auth/client/native_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from globus_sdk.authorizers import NullAuthorizer
from globus_sdk.response import GlobusHTTPResponse
from globus_sdk.scopes import Scope
from globus_sdk.transport import RequestsTransport, RetryConfig

from ..flow_managers import GlobusNativeAppFlowManager
from ..response import OAuthRefreshTokenResponse
Expand Down Expand Up @@ -39,15 +40,17 @@ def __init__(
environment: str | None = None,
base_url: str | None = None,
app_name: str | None = None,
transport_params: dict[str, t.Any] | None = None,
transport: RequestsTransport | None = None,
retry_config: RetryConfig | None = None,
) -> None:
super().__init__(
client_id=client_id,
authorizer=NullAuthorizer(),
environment=environment,
base_url=base_url,
app_name=app_name,
transport_params=transport_params,
transport=transport,
retry_config=retry_config,
)

def oauth2_start_flow(
Expand Down
7 changes: 5 additions & 2 deletions src/globus_sdk/services/auth/client/service_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from globus_sdk.authorizers import GlobusAuthorizer
from globus_sdk.response import GlobusHTTPResponse, IterableResponse
from globus_sdk.scopes import AuthScopes, Scope
from globus_sdk.transport import RequestsTransport, RetryConfig

if t.TYPE_CHECKING:
from globus_sdk.globus_app import GlobusApp
Expand Down Expand Up @@ -78,7 +79,8 @@ def __init__(
app_scopes: list[Scope] | None = None,
authorizer: GlobusAuthorizer | None = None,
app_name: str | None = None,
transport_params: dict[str, t.Any] | None = None,
transport: RequestsTransport | None = None,
retry_config: RetryConfig | None = None,
) -> None:
super().__init__(
environment=environment,
Expand All @@ -87,7 +89,8 @@ def __init__(
app_scopes=app_scopes,
authorizer=authorizer,
app_name=app_name,
transport_params=transport_params,
transport=transport,
retry_config=retry_config,
)

# FYI: this get_openid_configuration method is duplicated in AuthLoginBaseClient
Expand Down
7 changes: 5 additions & 2 deletions src/globus_sdk/services/flows/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
SpecificFlowScopes,
TransferScopes,
)
from globus_sdk.transport import RequestsTransport, RetryConfig

from .data import RunActivityNotificationPolicy
from .errors import FlowsAPIError
Expand Down Expand Up @@ -908,7 +909,8 @@ def __init__(
app_scopes: list[Scope] | None = None,
authorizer: GlobusAuthorizer | None = None,
app_name: str | None = None,
transport_params: dict[str, t.Any] | None = None,
transport: RequestsTransport | None = None,
retry_config: RetryConfig | None = None,
) -> None:
self._flow_id = flow_id
self.scopes = SpecificFlowScopes(flow_id)
Expand All @@ -918,7 +920,8 @@ def __init__(
environment=environment,
authorizer=authorizer,
app_name=app_name,
transport_params=transport_params,
transport=transport,
retry_config=retry_config,
)

@property
Expand Down
7 changes: 5 additions & 2 deletions src/globus_sdk/services/gcs/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from globus_sdk.authorizers import GlobusAuthorizer
from globus_sdk.globus_app import GlobusApp
from globus_sdk.scopes import GCSCollectionScopes, GCSEndpointScopes, Scope
from globus_sdk.transport import RequestsTransport, RetryConfig

from .data import (
CollectionDocument,
Expand Down Expand Up @@ -56,7 +57,8 @@ def __init__(
environment: str | None = None,
authorizer: GlobusAuthorizer | None = None,
app_name: str | None = None,
transport_params: dict[str, t.Any] | None = None,
transport: RequestsTransport | None = None,
retry_config: RetryConfig | None = None,
) -> None:
# check if the provided address was a DNS name or an HTTPS URL
if not gcs_address.startswith("https://"):
Expand All @@ -76,7 +78,8 @@ def __init__(
app_scopes=app_scopes,
authorizer=authorizer,
app_name=app_name,
transport_params=transport_params,
transport=transport,
retry_config=retry_config,
)

@staticmethod
Expand Down
Loading
Loading