Skip to content
Closed
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
44 changes: 43 additions & 1 deletion src/globus_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,24 @@ def __init__(
else:
self.environment = config.get_environment_name()

# initially, there is no owner for a client
# if an app is attached, it will be made into the owner
self._resource_owner: GlobusApp | None = None

# resolve the base_url for the client (see docstring for resolution precedence)
self.base_url = self._resolve_base_url(base_url, self.environment)

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()
# the resource_owner for a Transport is implicitly this client if and only if
# the client creates the Transport
if transport is not None:
self.transport = transport
else:
self.transport = RequestsTransport()
self.transport.resource_owner = self

log.debug(f"initialized transport of type {type(self.transport)}")

# setup paginated methods
Expand Down Expand Up @@ -260,6 +271,9 @@ def attach_globus_app(
if self.app_name is None:
self.app_name = app.app_name

# notate that the app owns the client
self.resource_owner = app

# finally, register the scope requirements on the app side
self._app.add_scope_requirements({self.resource_server: self.app_scopes})

Expand Down Expand Up @@ -307,6 +321,23 @@ def add_app_scope(

return self

@property
def resource_owner(self) -> GlobusApp | None:
"""
The GlobusApp which owns this client.
Defaults to whatever app was attached to the client, or None.
"""
return self._resource_owner

@resource_owner.setter
def resource_owner(self, value: GlobusApp | None) -> None:
if self._resource_owner is not None and self._resource_owner is not value:
self._resource_owner.owned_clients.remove(self)

self._resource_owner = value
if value is not None:
value.owned_clients.add(self)

@property
def app_name(self) -> str | None:
return self._app_name
Expand All @@ -331,6 +362,17 @@ def resource_server( # pylint: disable=missing-param-doc
return None
return self_or_cls.scopes.resource_server

def close(self) -> None:
"""
Close all resources which are owned by this client.

This closes any transport object attached to the client which lists the client
as its owner.
"""
if self.transport.resource_owner is self:
log.debug(f"closing transport for {type(self).__name__}")
self.transport.close()

def get( # pylint: disable=missing-param-doc
self,
path: str,
Expand Down
30 changes: 28 additions & 2 deletions src/globus_sdk/globus_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import abc
import contextlib
import copy
import logging
import typing as t
import uuid

Expand All @@ -26,6 +27,11 @@
from .config import DEFAULT_CONFIG, KNOWN_TOKEN_STORAGES, GlobusAppConfig
from .protocols import TokenStorageProvider

if t.TYPE_CHECKING:
from globus_sdk import BaseClient

log = logging.getLogger(__name__)


class GlobusApp(metaclass=abc.ABCMeta):
"""
Expand Down Expand Up @@ -73,6 +79,7 @@ def __init__(
) -> None:
self.app_name = app_name
self.config = config
self.owned_clients: set[BaseClient] = set()
self._token_validation_error_handling_enabled = True

self.client_id, self._login_client = self._resolve_client_info(
Expand Down Expand Up @@ -240,21 +247,25 @@ def _resolve_token_storage(
return token_storage

elif isinstance(token_storage, TokenStorageProvider):
return token_storage.for_globus_app(
token_storage = token_storage.for_globus_app(
app_name=app_name,
config=config,
client_id=client_id,
namespace=namespace,
)
token_storage.resource_owner = self
return token_storage

elif token_storage in KNOWN_TOKEN_STORAGES:
provider = KNOWN_TOKEN_STORAGES[token_storage]
return provider.for_globus_app(
token_storage = provider.for_globus_app(
app_name=app_name,
config=config,
client_id=client_id,
namespace=namespace,
)
token_storage.resource_owner = self
return token_storage

raise GlobusSDKUsageError(
f"Unsupported token_storage value: {token_storage}. Must be a "
Expand Down Expand Up @@ -357,6 +368,21 @@ def logout(self) -> None:
# Invalidate any cached authorizers
self._authorizer_factory.clear_cache()

def close(self) -> None:
"""
Close all resources currently held by the app.
This does not trigger a logout.
"""
for client in self.owned_clients:
if client.resource_owner is self:
log.debug(
f"closing app associated client of type {type(client).__name__}"
)
client.close()
if self.token_storage.resource_owner is self:
log.debug("closing app associated token storage")
self.token_storage.close()

@abc.abstractmethod
def _run_login_flow(
self, auth_params: GlobusAuthorizationParameters | None = None
Expand Down
10 changes: 9 additions & 1 deletion src/globus_sdk/token_storage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .token_data import TokenStorageData

if t.TYPE_CHECKING:
from globus_sdk.globus_app import GlobusAppConfig
from globus_sdk.globus_app import GlobusApp, GlobusAppConfig


class TokenStorage(metaclass=abc.ABCMeta):
Expand All @@ -32,6 +32,7 @@ class TokenStorage(metaclass=abc.ABCMeta):
"""

def __init__(self, namespace: str = "DEFAULT") -> None:
self.resource_owner: GlobusApp | None = None
self.namespace = namespace
self.id_token_decoder: globus_sdk.IDTokenDecoder | None = None

Expand Down Expand Up @@ -125,6 +126,13 @@ def _extract_identity_id(
else:
return None

def close(self) -> None: # noqa: B027
"""
Close any resources associated with this token storage.

By default, this does nothing, but subclasses may override it.
"""


class FileTokenStorage(TokenStorage, metaclass=abc.ABCMeta):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ def __init__(
)
super().__init__(namespace=token_storage.namespace)

def close(self) -> None:
"""Closing a validating storage closes the storage it contains."""
self.token_storage.close()

def _make_context(
self, token_data_by_resource_server: t.Mapping[str, TokenStorageData]
) -> TokenValidationContext:
Expand Down
4 changes: 4 additions & 0 deletions src/globus_sdk/transport/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
from .retry_check_runner import RetryCheckRunner
from .retry_config import RetryConfig

if t.TYPE_CHECKING:
from globus_sdk import BaseClient

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -66,6 +69,7 @@ def __init__(
verify_ssl: bool | str | pathlib.Path | None = None,
http_timeout: float | None = None,
) -> None:
self.resource_owner: BaseClient | None = None
self.session = requests.Session()
self.verify_ssl = config.get_ssl_verify(verify_ssl)
self.http_timeout = config.get_http_timeout(http_timeout)
Expand Down
Loading