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,4 @@
Changed
-------

- Remove support for normalizing nested iterables of scopes, e.g. ``[["scope1"], "scope2"]`` (:pr:`1259`)
10 changes: 0 additions & 10 deletions src/globus_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,11 @@
import typing as t
import uuid

if t.TYPE_CHECKING:
from globus_sdk.scopes import Scope


# these types are aliases meant for internal use
IntLike = t.Union[int, str]
UUIDLike = t.Union[uuid.UUID, str]
DateLike = t.Union[str, datetime.datetime]

ScopeCollectionType = t.Union[
str,
"Scope",
t.Iterable["ScopeCollectionType"],
]


class ResponseLike(t.Protocol):
@property
Expand Down
5 changes: 2 additions & 3 deletions src/globus_sdk/authorizers/client_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import typing as t

import globus_sdk
from globus_sdk._types import ScopeCollectionType
from globus_sdk.scopes import scopes_to_str
from globus_sdk.scopes import Scope, scopes_to_str

from .renewing import RenewingAuthorizer

Expand Down Expand Up @@ -58,7 +57,7 @@ class ClientCredentialsAuthorizer(
def __init__(
self,
confidential_client: globus_sdk.ConfidentialAppAuthClient,
scopes: ScopeCollectionType,
scopes: str | Scope | t.Iterable[str | Scope],
*,
access_token: str | None = None,
expires_at: int | None = None,
Expand Down
10 changes: 6 additions & 4 deletions src/globus_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from globus_sdk import GlobusSDKUsageError, config, exc
from globus_sdk._classproperty import classproperty
from globus_sdk._types import ScopeCollectionType
from globus_sdk._utils import slash_join
from globus_sdk.authorizers import GlobusAuthorizer
from globus_sdk.paging import PaginatorTable
Expand Down Expand Up @@ -252,7 +251,9 @@ def attach_globus_app(
# finally, register the scope requirements on the app side
self._app.add_scope_requirements({self.resource_server: self.app_scopes})

def add_app_scope(self, scope_collection: ScopeCollectionType) -> BaseClient:
def add_app_scope(
self, scope_collection: str | Scope | t.Iterable[str | Scope]
) -> BaseClient:
"""
Add a given scope collection to this client's ``GlobusApp`` scope requirements
for this client's ``resource_server``. This allows defining additional scope
Expand All @@ -263,8 +264,9 @@ def add_app_scope(self, scope_collection: ScopeCollectionType) -> BaseClient:
Raises ``GlobusSDKUsageError`` if this client was not initialized with a
``GlobusApp``.

:param scope_collection: A scope or scopes of ``ScopeCollectionType`` to be
added to the app's required scopes.
:param scope_collection: A scope or scopes of
``str | Scope | t.Iterable[str | Scope]`` to be added to
the app's required scopes.

.. tab-set::

Expand Down
13 changes: 9 additions & 4 deletions src/globus_sdk/globus_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
GlobusSDKUsageError,
IDTokenDecoder,
)
from globus_sdk._types import ScopeCollectionType, UUIDLike
from globus_sdk._types import UUIDLike
from globus_sdk.authorizers import GlobusAuthorizer
from globus_sdk.gare import GlobusAuthorizationParameters
from globus_sdk.scopes import AuthScopes, Scope, ScopeParser, scopes_to_scope_list
Expand Down Expand Up @@ -66,7 +66,9 @@ def __init__(
login_client: AuthLoginClient | None = None,
client_id: UUIDLike | None = None,
client_secret: str | None = None,
scope_requirements: t.Mapping[str, ScopeCollectionType] | None = None,
scope_requirements: (
t.Mapping[str, str | Scope | t.Iterable[str | Scope]] | None
) = None,
config: GlobusAppConfig = DEFAULT_CONFIG,
) -> None:
self.app_name = app_name
Expand Down Expand Up @@ -119,7 +121,10 @@ def __init__(
consent_client.attach_globus_app(self, app_scopes=[AuthScopes.openid])

def _resolve_scope_requirements(
self, scope_requirements: t.Mapping[str, ScopeCollectionType] | None
self,
scope_requirements: (
t.Mapping[str, str | Scope | t.Iterable[str | Scope]] | None
),
) -> dict[str, list[Scope]]:
if scope_requirements is None:
return {}
Expand Down Expand Up @@ -408,7 +413,7 @@ def _disabled_token_validation_error_handler(self) -> t.Iterator[None]:
self._token_validation_error_handling_enabled = initial_val

def add_scope_requirements(
self, scope_requirements: t.Mapping[str, ScopeCollectionType]
self, scope_requirements: t.Mapping[str, str | Scope | t.Iterable[str | Scope]]
) -> None:
"""
Add given scope requirements to the app's scope requirements. Any duplicate
Expand Down
9 changes: 7 additions & 2 deletions src/globus_sdk/globus_app/client_app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

import typing as t

from globus_sdk import AuthLoginClient, ConfidentialAppAuthClient, GlobusSDKUsageError
from globus_sdk._types import ScopeCollectionType, UUIDLike
from globus_sdk._types import UUIDLike
from globus_sdk.gare import GlobusAuthorizationParameters
from globus_sdk.scopes import Scope

from .app import GlobusApp
from .authorizer_factory import ClientCredentialsAuthorizerFactory
Expand Down Expand Up @@ -57,7 +60,9 @@ def __init__(
login_client: ConfidentialAppAuthClient | None = None,
client_id: UUIDLike | None = None,
client_secret: str | None = None,
scope_requirements: dict[str, ScopeCollectionType] | None = None,
scope_requirements: (
dict[str, str | Scope | t.Iterable[str | Scope]] | None
) = None,
config: GlobusAppConfig = DEFAULT_CONFIG,
) -> None:
if config.login_flow_manager is not None:
Expand Down
6 changes: 4 additions & 2 deletions src/globus_sdk/globus_app/user_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
NativeAppAuthClient,
Scope,
)
from globus_sdk._types import ScopeCollectionType, UUIDLike
from globus_sdk._types import UUIDLike
from globus_sdk.gare import GlobusAuthorizationParameters
from globus_sdk.login_flows import CommandLineLoginFlowManager, LoginFlowManager
from globus_sdk.token_storage import (
Expand Down Expand Up @@ -80,7 +80,9 @@ def __init__(
login_client: AuthLoginClient | None = None,
client_id: UUIDLike | None = None,
client_secret: str | None = None,
scope_requirements: t.Mapping[str, ScopeCollectionType] | None = None,
scope_requirements: (
t.Mapping[str, str | Scope | t.Iterable[str | Scope]] | None
) = None,
config: GlobusAppConfig = DEFAULT_CONFIG,
) -> None:
super().__init__(
Expand Down
46 changes: 28 additions & 18 deletions src/globus_sdk/scopes/_normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
from .parser import ScopeParser
from .representation import Scope

if t.TYPE_CHECKING:
from globus_sdk._types import ScopeCollectionType


def scopes_to_str(scopes: ScopeCollectionType) -> str:
def scopes_to_str(scopes: str | Scope | t.Iterable[str | Scope]) -> str:
"""
Normalize a scope collection to a space-separated scope string.
Normalize scopes to a space-separated scope string.

:param scopes: A scope string or object, or an iterable of scope strings or objects.
:param scopes: A scope string, scope object, or an iterable of scope strings
and scope objects.
:returns: A space-separated scope string.

Example usage:
Expand All @@ -29,11 +27,12 @@ def scopes_to_str(scopes: ScopeCollectionType) -> str:
return " ".join(str(scope) for scope in scope_iter)


def scopes_to_scope_list(scopes: ScopeCollectionType) -> list[Scope]:
def scopes_to_scope_list(scopes: str | Scope | t.Iterable[str | Scope]) -> list[Scope]:
"""
Normalize a scope collection to a list of Scope objects.
Normalize scopes to a list of Scope objects.

:param scopes: A scope string or object, or an iterable of scope strings or objects.
:param scopes: A scope string, scope object, or an iterable of scope strings
and scope objects.
:returns: A list of Scope objects.

Example usage:
Expand All @@ -55,19 +54,19 @@ def scopes_to_scope_list(scopes: ScopeCollectionType) -> list[Scope]:


def _iter_scope_collection(
obj: ScopeCollectionType,
obj: str | Scope | t.Iterable[str | Scope],
*,
split_root_scopes: bool = True,
) -> t.Iterator[str | Scope]:
"""
Provide an iterator over a scope collection type, flattening nested scope
collections as encountered.
Provide an iterator over a collection of scopes.

Collections of scope representations are yielded one at a time.
Individual scope representations are yielded as-is.

:param obj: A scope collection or scope representation.
:param iter_scope_strings: If True, scope strings with multiple root scopes are
:param obj: A scope string, scope object, or an iterable of scope strings
and scope objects.
:param split_root_scopes: If True, scope strings with multiple root scopes are

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, kudos for spotting and fixing this!

split. This flag allows a caller to optimize, skipping a bfs operation if
merging will be done later purely with strings.

Expand All @@ -77,20 +76,19 @@ def _iter_scope_collection(

>>> list(_iter_scope_collection("foo"))
['foo']
>>> list(_iter_scope_collection(Scope.parse("foo bar"), "baz qux"))
>>> list(_iter_scope_collection(Scope.parse("foo bar") + ["baz qux"]))
[Scope('foo'), Scope('bar'), 'baz', 'qux']
>>> list(_iter_scope_collection("foo bar[baz qux]"))
['foo', 'bar[baz qux]']
>>> list(_iter_scope_collection("foo bar[baz qux]", split_root_scopes=False))
'foo bar[baz qux]'
['foo bar[baz qux]']
"""
if isinstance(obj, str):
yield from _iter_scope_string(obj, split_root_scopes)
elif isinstance(obj, Scope):
yield obj
else:
for item in obj:
yield from _iter_scope_collection(item, split_root_scopes=split_root_scopes)
yield from _iter_scope_iterable(obj, split_root_scopes)


def _iter_scope_string(scope_str: str, split_root_scopes: bool) -> t.Iterator[str]:
Expand All @@ -102,3 +100,15 @@ def _iter_scope_string(scope_str: str, split_root_scopes: bool) -> t.Iterator[st
else:
for scope_obj in ScopeParser.parse(scope_str):
yield str(scope_obj)


def _iter_scope_iterable(
scope_iterable: t.Iterable[str | Scope], split_root_scopes: bool
) -> t.Iterator[str | Scope]:
for scope in scope_iterable:
if isinstance(scope, str):
yield from _iter_scope_string(scope, split_root_scopes)
elif isinstance(scope, Scope):
yield scope
else:
raise TypeError(f"Expected str or Scope in iterable, got {type(scope)}")
7 changes: 4 additions & 3 deletions src/globus_sdk/services/auth/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey

from globus_sdk._missing import MISSING, MissingType
from globus_sdk._types import ScopeCollectionType
from globus_sdk.exc import GlobusSDKUsageError
from globus_sdk.response import GlobusHTTPResponse
from globus_sdk.scopes import scopes_to_str
from globus_sdk.scopes import Scope, scopes_to_str

log = logging.getLogger(__name__)


def stringify_requested_scopes(requested_scopes: ScopeCollectionType) -> str:
def stringify_requested_scopes(
requested_scopes: str | Scope | t.Iterable[str | Scope],
) -> str:
requested_scopes_string: str = scopes_to_str(requested_scopes)
if requested_scopes_string == "":
raise GlobusSDKUsageError(
Expand Down
7 changes: 4 additions & 3 deletions src/globus_sdk/services/auth/client/confidential_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from globus_sdk import exc
from globus_sdk._missing import MISSING, MissingType
from globus_sdk._remarshal import commajoin, strseq_iter, strseq_listify
from globus_sdk._types import ScopeCollectionType, UUIDLike
from globus_sdk._types import UUIDLike
from globus_sdk.authorizers import BasicAuthorizer
from globus_sdk.response import GlobusHTTPResponse
from globus_sdk.scopes import Scope

from .._common import stringify_requested_scopes
from ..flow_managers import GlobusAuthorizationCodeFlowManager
Expand Down Expand Up @@ -106,7 +107,7 @@ def get_identities(
)

def oauth2_client_credentials_tokens(
self, requested_scopes: ScopeCollectionType
self, requested_scopes: str | Scope | t.Iterable[str | Scope]
) -> OAuthClientCredentialsResponse:
r"""
Perform an OAuth2 Client Credentials Grant to get access tokens which
Expand Down Expand Up @@ -142,7 +143,7 @@ def oauth2_client_credentials_tokens(
def oauth2_start_flow(
self,
redirect_uri: str,
requested_scopes: ScopeCollectionType,
requested_scopes: str | Scope | t.Iterable[str | Scope],
*,
state: str = "_default",
refresh_tokens: bool = False,
Expand Down
5 changes: 3 additions & 2 deletions src/globus_sdk/services/auth/client/native_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import typing as t

from globus_sdk._missing import MISSING, MissingType
from globus_sdk._types import ScopeCollectionType, UUIDLike
from globus_sdk._types import UUIDLike
from globus_sdk.authorizers import NullAuthorizer
from globus_sdk.response import GlobusHTTPResponse
from globus_sdk.scopes import Scope

from ..flow_managers import GlobusNativeAppFlowManager
from ..response import OAuthRefreshTokenResponse
Expand Down Expand Up @@ -51,7 +52,7 @@ def __init__(

def oauth2_start_flow(
self,
requested_scopes: ScopeCollectionType,
requested_scopes: str | Scope | t.Iterable[str | Scope],
*,
redirect_uri: str | MissingType = MISSING,
state: str = "_default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import urllib.parse

from globus_sdk._missing import filter_missing
from globus_sdk._types import ScopeCollectionType
from globus_sdk._utils import slash_join
from globus_sdk.scopes import Scope

from .._common import stringify_requested_scopes
from ..response import OAuthAuthorizationCodeResponse
Expand Down Expand Up @@ -50,7 +50,7 @@ def __init__(
self,
auth_client: globus_sdk.ConfidentialAppAuthClient,
redirect_uri: str,
requested_scopes: ScopeCollectionType,
requested_scopes: str | Scope | t.Iterable[str | Scope],
state: str = "_default",
refresh_tokens: bool = False,
) -> None:
Expand Down
4 changes: 2 additions & 2 deletions src/globus_sdk/services/auth/flow_managers/native_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import urllib.parse

from globus_sdk._missing import MISSING, MissingType, filter_missing
from globus_sdk._types import ScopeCollectionType
from globus_sdk._utils import slash_join
from globus_sdk.exc import GlobusSDKUsageError
from globus_sdk.scopes import Scope

from .._common import stringify_requested_scopes
from ..response import OAuthAuthorizationCodeResponse
Expand Down Expand Up @@ -103,7 +103,7 @@ class GlobusNativeAppFlowManager(GlobusOAuthFlowManager):
def __init__(
self,
auth_client: globus_sdk.NativeAppAuthClient,
requested_scopes: ScopeCollectionType,
requested_scopes: str | Scope | t.Iterable[str | Scope],
redirect_uri: str | MissingType = MISSING,
state: str = "_default",
verifier: str | MissingType = MISSING,
Expand Down
13 changes: 7 additions & 6 deletions tests/non-pytest/mypy-ignore-tests/app_scope_requirements.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from globus_sdk import UserApp

# declare scope data in the form of a subtype of the ScopeCollectionType (`list[str]`)
# indexed in a dict, this is meant to be a subtype of the requirements data accepted
# by `GlobusApp.add_scope_requirements`
# declare scope data in the form of a subtype of the
# `str | Scope | t.Iterable[str | Scope]` (`list[str]`) indexed in a dict,
# this is meant to be a subtype of the requirements data accepted by
# `GlobusApp.add_scope_requirements`
#
# this is a regression test for that being annotated as `dict[str, ScopeCollectionType]`
# which will reject the input type because `dict` is a mutable container, and therefore
# invariant
# this is a regression test for that being annotated as
# `dict[str, str | Scope | t.Iterable[str | Scope]]` which will reject the input
# type because `dict` is a mutable container, and therefore invariant
scopes: dict[str, list[str]] = {"foo": ["bar"]}
my_app = UserApp("...", client_id="...")
my_app.add_scope_requirements(scopes)
Loading
Loading