From 614af28f036889b8dbff00d52ffbe12c4355c38d Mon Sep 17 00:00:00 2001 From: m1yag1 <8730430+m1yag1@users.noreply.github.com> Date: Thu, 1 May 2025 16:25:11 -0500 Subject: [PATCH 001/176] Set version to 4.0.0a1 for v4 development --- src/globus_sdk/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globus_sdk/version.py b/src/globus_sdk/version.py index 423fe96bb..47cf67f48 100644 --- a/src/globus_sdk/version.py +++ b/src/globus_sdk/version.py @@ -1,3 +1,3 @@ # single source of truth for package version, # see https://packaging.python.org/en/latest/single_source_version/ -__version__ = "3.55.0" +__version__ = "4.0.0a1" From 86522c9d524e1bc9283f05b64b74d3bfae7c5d76 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 12 May 2025 17:31:46 -0500 Subject: [PATCH 002/176] Configure 'Breaking Changes' changelog section (#1187) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4ca200796..40281e707 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,6 +156,7 @@ entry_title_template = 'v{{ version }} ({{ date.strftime("%Y-%m-%d") }})' rst_header_chars = "-~" categories = [ "Python Support", + "Breaking Changes", "Added", "Removed", "Changed", From 0633eafe758ce09f969ddb974c668b10bbfeb8f1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 12 May 2025 17:32:09 -0500 Subject: [PATCH 003/176] Remove the "default requested scopes" (#1186) * Remove the "default requested scopes" These were the default for login flows and client credentials. When GlobusApp-driven logins are used, these defaults are not used. They were only applied for direct use of the login flow and token request mechanisms for login clients. One log line has been changed to produce slightly more useful debug-level logs. Also, introduce the first v3.x -> v4 upgrading section to the upgrading doc. * Fix typing errors and raise on unreachable types Fix typing errors where `... | None` was passed for `requested_scopes`. Additionally, to handle cases in GlobusApp, raise usage errors if an app is ever doing a login without provided scopes in the authorization parameters. These conditions aren't unreachable based on the types which are used, but a user who configures things to reach these conditions is engaged in explicitly unsupported usage (e.g., trying to give explicit parameters to a client app which don't include scopes). * Retitle changelog section --- ...2_143605_sirosen_remove_default_scopes.rst | 8 +++++ docs/upgrading.rst | 36 +++++++++++++++++++ src/globus_sdk/globus_app/client_app.py | 4 +++ .../login_flows/login_flow_manager.py | 6 ++++ src/globus_sdk/services/auth/_common.py | 21 ++--------- .../auth/client/confidential_client.py | 28 ++++++++------- .../services/auth/client/native_client.py | 2 +- .../auth/flow_managers/authorization_code.py | 5 ++- .../services/auth/flow_managers/native_app.py | 6 ++-- .../services/auth/test_auth_client_flow.py | 23 +++++------- .../unit/helpers/test_auth_scope_stringify.py | 11 +----- 11 files changed, 87 insertions(+), 63 deletions(-) create mode 100644 changelog.d/20250512_143605_sirosen_remove_default_scopes.rst diff --git a/changelog.d/20250512_143605_sirosen_remove_default_scopes.rst b/changelog.d/20250512_143605_sirosen_remove_default_scopes.rst new file mode 100644 index 000000000..be02c475a --- /dev/null +++ b/changelog.d/20250512_143605_sirosen_remove_default_scopes.rst @@ -0,0 +1,8 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- The SDK no longer sets default scopes for direct use of client + credentials and auth client login flow methods. Users should either use + ``GlobusApp`` objects, which can specify scopes based on the clients in use, + or else pass a list of scopes explicitly to + ``oauth2_client_credentials_tokens`` or ``oauth2_start_flow``. (:pr:`1186`) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index dd0a541ee..3d2b59860 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -42,6 +42,42 @@ Then, code can dispatch with else: pass # do another +From 3.x to 4.0 +--------------- + +``requested_scopes`` is Required +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several methods have historically taken an optional parameter, +``requested_scopes``. + +- ``ConfidentialAppAuthClient.oauth2_client_credentials_tokens`` +- ``ConfidentialAppAuthClient.oauth2_start_flow`` +- ``NativeAppAuthClient.oauth2_start_flow`` + +In previous versions of the SDK, these methods provided a default value for +``requested_scopes`` of +``"openid profile email urn:globus:auth:scopes:transfer.api.globus.org:all"``. +This default has now been removed and users should always specify the scopes +they need when using these methods. + +Users of ``GlobusApp`` constructs (``UserApp`` and ``ClientApp``) do not need +to update their usage. + +The default could only be used by applications which only use Globus Transfer +and Globus Auth. +Change: + +.. code-block:: python + + # globus-sdk v3 + auth_client.oauth2_start_flow() + authorize_url = auth_client.oauth2_get_authorize_url() + + # globus-sdk v4 + auth_client.oauth2_start_flow(requested_scopes=globus_sdk.TransferClient.scopes.all) + authorize_url = auth_client.oauth2_get_authorize_url() + From 1.x or 2.x to 3.0 ----------------------- diff --git a/src/globus_sdk/globus_app/client_app.py b/src/globus_sdk/globus_app/client_app.py index f1f0082f1..de145d467 100644 --- a/src/globus_sdk/globus_app/client_app.py +++ b/src/globus_sdk/globus_app/client_app.py @@ -117,6 +117,10 @@ def _run_login_flow( only the required_scopes parameter is used. """ auth_params = self._auth_params_with_required_scopes(auth_params) + if not auth_params.required_scopes: + raise GlobusSDKUsageError( + "A ClientApp cannot get tokens without configured required scopes." + ) token_response = self._login_client.oauth2_client_credentials_tokens( requested_scopes=auth_params.required_scopes ) diff --git a/src/globus_sdk/login_flows/login_flow_manager.py b/src/globus_sdk/login_flows/login_flow_manager.py index 79e481f4c..e37c358ef 100644 --- a/src/globus_sdk/login_flows/login_flow_manager.py +++ b/src/globus_sdk/login_flows/login_flow_manager.py @@ -70,6 +70,12 @@ def _oauth2_start_flow( """ login_client = self.login_client requested_scopes = auth_parameters.required_scopes + if not requested_scopes: + raise globus_sdk.GlobusSDKUsageError( + f"{type(self).__name__} cannot start a login flow without scopes " + "in the authorization parameters." + ) + # Native and Confidential App clients have different signatures for this method, # so they must be type checked & called independently. if isinstance(login_client, globus_sdk.NativeAppAuthClient): diff --git a/src/globus_sdk/services/auth/_common.py b/src/globus_sdk/services/auth/_common.py index 7cbec0610..2f417c468 100644 --- a/src/globus_sdk/services/auth/_common.py +++ b/src/globus_sdk/services/auth/_common.py @@ -9,30 +9,13 @@ from globus_sdk._types import ScopeCollectionType from globus_sdk.exc import GlobusSDKUsageError -from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.response import GlobusHTTPResponse -from globus_sdk.scopes import AuthScopes, TransferScopes, scopes_to_str +from globus_sdk.scopes import scopes_to_str log = logging.getLogger(__name__) -_DEFAULT_REQUESTED_SCOPES = ( - AuthScopes.openid, - AuthScopes.profile, - AuthScopes.email, - TransferScopes.all, -) - - -def stringify_requested_scopes(requested_scopes: ScopeCollectionType | None) -> str: - if requested_scopes is None: - warn_deprecated( - "`requested_scopes` was not specified or was given as `None`. " - "A default set of scopes will be used, but this behavior is deprecated. " - "Specify an explicit set of scopes instead.", - stacklevel=3, - ) - requested_scopes = _DEFAULT_REQUESTED_SCOPES +def stringify_requested_scopes(requested_scopes: ScopeCollectionType) -> str: requested_scopes_string: str = scopes_to_str(requested_scopes) if requested_scopes_string == "": raise GlobusSDKUsageError( diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index eb81c61bf..eb321e55f 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -107,8 +107,7 @@ def get_identities( ) def oauth2_client_credentials_tokens( - self, - requested_scopes: ScopeCollectionType | None = None, + self, requested_scopes: ScopeCollectionType ) -> OAuthClientCredentialsResponse: r""" Perform an OAuth2 Client Credentials Grant to get access tokens which @@ -117,20 +116,25 @@ def oauth2_client_credentials_tokens( This method does not use a ``GlobusOAuthFlowManager`` because it is not at all necessary to do so. - :param requested_scopes: The scopes on the token(s) being requested. Defaults to - ``openid profile email urn:globus:auth:scope:transfer.api.globus.org:all`` + :param requested_scopes: The scopes on the token(s) being requested. For example, with a Client ID of "CID1001" and a Client Secret of "RAND2002", you could use this grant type like so: - >>> client = ConfidentialAppAuthClient("CID1001", "RAND2002") - >>> tokens = client.oauth2_client_credentials_tokens() - >>> transfer_token_info = ( - ... tokens.by_resource_server["transfer.api.globus.org"]) - >>> transfer_token = transfer_token_info["access_token"] - """ - log.debug("Fetching token(s) using client credentials") + .. code-block:: pycon + + >>> client = ConfidentialAppAuthClient("CID1001", "RAND2002") + >>> tokens = client.oauth2_client_credentials_tokens( + ... "urn:globus:auth:scope:transfer.api.globus.org:all" + ... ) + >>> transfer_token_info = tokens.by_resource_server["transfer.api.globus.org"] + >>> transfer_token = transfer_token_info["access_token"] + """ # noqa: E501 requested_scopes_string = stringify_requested_scopes(requested_scopes) + log.debug( + "Fetching token(s) using client credentials, " + f"scope={requested_scopes_string}" + ) return self.oauth2_token( {"grant_type": "client_credentials", "scope": requested_scopes_string}, response_class=OAuthClientCredentialsResponse, @@ -139,7 +143,7 @@ def oauth2_client_credentials_tokens( def oauth2_start_flow( self, redirect_uri: str, - requested_scopes: ScopeCollectionType | None = None, + requested_scopes: ScopeCollectionType, *, state: str = "_default", refresh_tokens: bool = False, diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index 5b4181dd1..b63eaff2b 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -50,7 +50,7 @@ def __init__( def oauth2_start_flow( self, - requested_scopes: ScopeCollectionType | None = None, + requested_scopes: ScopeCollectionType, *, redirect_uri: str | None = None, state: str = "_default", diff --git a/src/globus_sdk/services/auth/flow_managers/authorization_code.py b/src/globus_sdk/services/auth/flow_managers/authorization_code.py index 939622494..0a1cbac28 100644 --- a/src/globus_sdk/services/auth/flow_managers/authorization_code.py +++ b/src/globus_sdk/services/auth/flow_managers/authorization_code.py @@ -36,8 +36,7 @@ class GlobusAuthorizationCodeFlowManager(GlobusOAuthFlowManager): and also to make calls to the Auth service. :param redirect_uri: The page that users should be directed to after authenticating at the authorize URL. - :param requested_scopes: The scopes on the token(s) being requested. Defaults to - ``openid profile email urn:globus:auth:scope:transfer.api.globus.org:all`` + :param requested_scopes: The scopes on the token(s) being requested. :param state: This string allows an application to pass information back to itself in the course of the OAuth flow. Because the user will navigate away from the application to complete the flow, this parameter lets the app pass an arbitrary @@ -50,7 +49,7 @@ def __init__( self, auth_client: globus_sdk.ConfidentialAppAuthClient, redirect_uri: str, - requested_scopes: ScopeCollectionType | None = None, + requested_scopes: ScopeCollectionType, state: str = "_default", refresh_tokens: bool = False, ) -> None: diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index 0df0b0552..d4c21342e 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -80,8 +80,7 @@ class GlobusNativeAppFlowManager(GlobusOAuthFlowManager): :param auth_client: The client object on which this flow is based. It is used to extract default values for the flow, and also to make calls to the Auth service. - :param requested_scopes: The scopes on the token(s) being requested. Defaults to - ``openid profile email urn:globus:auth:scope:transfer.api.globus.org:all`` + :param requested_scopes: The scopes on the token(s) being requested. :param redirect_uri: The page that users should be directed to after authenticating at the authorize URL. Defaults to 'https://auth.globus.org/v2/web/auth-code', which displays the resulting ``auth_code`` for users to copy-paste back into @@ -101,7 +100,7 @@ class GlobusNativeAppFlowManager(GlobusOAuthFlowManager): def __init__( self, auth_client: globus_sdk.NativeAppAuthClient, - requested_scopes: ScopeCollectionType | None = None, + requested_scopes: ScopeCollectionType, redirect_uri: str | None = None, state: str = "_default", verifier: str | None = None, @@ -123,7 +122,6 @@ def __init__( ) # convert scopes iterable to string immediately on load - # and default to the default requested scopes self.requested_scopes = stringify_requested_scopes(requested_scopes) # default to `/v2/web/auth-code` on whatever environment we're looking diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index 7a37c19b8..367ed58b7 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -180,12 +180,10 @@ def test_oauth2_get_authorize_url_supports_session_params( def test_oauth2_get_authorize_url_native_defaults(native_client): - # default parameters for starting auth flow - # should warn because scopes were not specified - with pytest.warns(globus_sdk.RemovedInV4Warning): - flow_manager = globus_sdk.services.auth.GlobusNativeAppFlowManager( - native_client - ) + flow_manager = globus_sdk.services.auth.GlobusNativeAppFlowManager( + native_client, + TransferScopes.all, + ) native_client.current_oauth2_flow_manager = flow_manager # get url and validate results @@ -197,7 +195,7 @@ def test_oauth2_get_authorize_url_native_defaults(native_client): assert parsed_params == { "client_id": [native_client.client_id], "redirect_uri": [native_client.base_url + "v2/web/auth-code"], - "scope": [f"openid profile email {TransferScopes.all}"], + "scope": [TransferScopes.all], "state": ["_default"], "response_type": ["code"], "code_challenge": [flow_manager.challenge], @@ -238,12 +236,9 @@ def test_oauth2_get_authorize_url_native_custom_params(native_client): def test_oauth2_get_authorize_url_confidential_defaults(confidential_client): - # default parameters for starting auth flow - # warns because no requested_scopes was passed - with pytest.warns(globus_sdk.RemovedInV4Warning): - flow_manager = globus_sdk.services.auth.GlobusAuthorizationCodeFlowManager( - confidential_client, "uri" - ) + flow_manager = globus_sdk.services.auth.GlobusAuthorizationCodeFlowManager( + confidential_client, "uri", TransferScopes.all + ) confidential_client.current_oauth2_flow_manager = flow_manager # get url_and validate results @@ -255,7 +250,7 @@ def test_oauth2_get_authorize_url_confidential_defaults(confidential_client): assert parsed_params == { "client_id": [confidential_client.client_id], "redirect_uri": ["uri"], - "scope": [f"openid profile email {TransferScopes.all}"], + "scope": [TransferScopes.all], "state": ["_default"], "response_type": ["code"], "access_type": ["online"], diff --git a/tests/unit/helpers/test_auth_scope_stringify.py b/tests/unit/helpers/test_auth_scope_stringify.py index ace749620..09d3a77f9 100644 --- a/tests/unit/helpers/test_auth_scope_stringify.py +++ b/tests/unit/helpers/test_auth_scope_stringify.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk import GlobusSDKUsageError, RemovedInV4Warning +from globus_sdk import GlobusSDKUsageError from globus_sdk.scopes import MutableScope from globus_sdk.services.auth._common import stringify_requested_scopes @@ -32,12 +32,3 @@ def test_scope_stringify_rejects_empty_collection(collection_obj): match="requested_scopes cannot be the empty string or empty collection", ): stringify_requested_scopes(collection_obj) - - -def test_scope_stringify_handles_none_with_default(): - with pytest.warns(RemovedInV4Warning, match="Specify an explicit set of scopes"): - scope_string = stringify_requested_scopes(None) - assert ( - scope_string - == "openid profile email urn:globus:auth:scope:transfer.api.globus.org:all" - ) From 45b6909ffc8779da20de94c6acec1063d6064192 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Tue, 13 May 2025 11:01:15 -0500 Subject: [PATCH 004/176] Remove base_path from all client classes (#1185) * Removed base_path from client classes * Removed _base_path_map from RegisteredResponse * Removed trailing slash enforcement * Added changelog --- ...sc_26346_remove_base_path_from_clients.rst | 5 + .../_testing/data/groups/create_group.py | 2 +- .../_testing/data/groups/delete_group.py | 2 +- .../_testing/data/groups/get_group.py | 4 +- .../groups/get_group_by_subscription_id.py | 2 +- .../_testing/data/groups/get_my_groups.py | 2 +- .../data/groups/set_group_policies.py | 2 +- .../_testing/data/transfer/create_endpoint.py | 4 +- .../transfer/endpoint_manager_task_list.py | 2 +- ...point_manager_task_successful_transfers.py | 2 +- .../_testing/data/transfer/get_endpoint.py | 2 +- .../data/transfer/get_submission_id.py | 2 +- .../_testing/data/transfer/operation_mkdir.py | 4 +- .../data/transfer/operation_rename.py | 4 +- .../_testing/data/transfer/operation_stat.py | 10 +- .../data/transfer/set_subscription_id.py | 12 +- .../_testing/data/transfer/submit_delete.py | 6 +- .../_testing/data/transfer/submit_transfer.py | 10 +- .../_testing/data/transfer/task_list.py | 2 +- .../_testing/data/transfer/update_endpoint.py | 4 +- src/globus_sdk/_testing/models.py | 17 +- src/globus_sdk/client.py | 13 -- src/globus_sdk/services/groups/client.py | 31 ++-- src/globus_sdk/services/transfer/client.py | 168 ++++++++++-------- .../services/groups/test_group_memberships.py | 2 +- .../services/transfer/test_operation_ls.py | 2 +- .../transfer/test_operation_symlink.py | 2 +- tests/unit/test_base_client.py | 48 +---- tests/unit/test_gcs_client.py | 20 +-- 29 files changed, 180 insertions(+), 206 deletions(-) create mode 100644 changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst diff --git a/changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst b/changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst new file mode 100644 index 000000000..80ec1266e --- /dev/null +++ b/changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst @@ -0,0 +1,5 @@ +Removed +~~~~~~~ + +- SDK client classes no longer define nor prepend a ``base_path`` attribute which they prefix to paths. + Make sure to use the full path now when using client methods. (:pr:`1185`) \ No newline at end of file diff --git a/src/globus_sdk/_testing/data/groups/create_group.py b/src/globus_sdk/_testing/data/groups/create_group.py index 2566fbad2..513ba0888 100644 --- a/src/globus_sdk/_testing/data/groups/create_group.py +++ b/src/globus_sdk/_testing/data/groups/create_group.py @@ -6,7 +6,7 @@ metadata={"group_id": GROUP_ID}, default=RegisteredResponse( service="groups", - path="/groups", + path="/v2/groups", method="POST", json=BASE_GROUP_DOC, ), diff --git a/src/globus_sdk/_testing/data/groups/delete_group.py b/src/globus_sdk/_testing/data/groups/delete_group.py index 35fcee46f..a055ede33 100644 --- a/src/globus_sdk/_testing/data/groups/delete_group.py +++ b/src/globus_sdk/_testing/data/groups/delete_group.py @@ -6,7 +6,7 @@ metadata={"group_id": GROUP_ID}, default=RegisteredResponse( service="groups", - path=f"/groups/{GROUP_ID}", + path=f"/v2/groups/{GROUP_ID}", method="DELETE", json=BASE_GROUP_DOC, ), diff --git a/src/globus_sdk/_testing/data/groups/get_group.py b/src/globus_sdk/_testing/data/groups/get_group.py index 98d275180..4ff0635ee 100644 --- a/src/globus_sdk/_testing/data/groups/get_group.py +++ b/src/globus_sdk/_testing/data/groups/get_group.py @@ -12,12 +12,12 @@ metadata={"group_id": GROUP_ID}, default=RegisteredResponse( service="groups", - path=f"/groups/{GROUP_ID}", + path=f"/v2/groups/{GROUP_ID}", json=BASE_GROUP_DOC, ), subscription=RegisteredResponse( service="groups", - path=f"/groups/{SUBSCRIPTION_GROUP_ID}", + path=f"/v2/groups/{SUBSCRIPTION_GROUP_ID}", json=SUBSCRIPTION_GROUP_DOC, metadata={ "group_id": SUBSCRIPTION_GROUP_ID, diff --git a/src/globus_sdk/_testing/data/groups/get_group_by_subscription_id.py b/src/globus_sdk/_testing/data/groups/get_group_by_subscription_id.py index 12147be60..53ab43215 100644 --- a/src/globus_sdk/_testing/data/groups/get_group_by_subscription_id.py +++ b/src/globus_sdk/_testing/data/groups/get_group_by_subscription_id.py @@ -6,7 +6,7 @@ metadata={"group_id": SUBSCRIPTION_GROUP_ID, "subscription_id": SUBSCRIPTION_ID}, default=RegisteredResponse( service="groups", - path=f"/subscription_info/{SUBSCRIPTION_ID}", + path=f"/v2/subscription_info/{SUBSCRIPTION_ID}", json={ "group_id": SUBSCRIPTION_GROUP_ID, "subscription_id": SUBSCRIPTION_ID, diff --git a/src/globus_sdk/_testing/data/groups/get_my_groups.py b/src/globus_sdk/_testing/data/groups/get_my_groups.py index c25f1a545..73523d28e 100644 --- a/src/globus_sdk/_testing/data/groups/get_my_groups.py +++ b/src/globus_sdk/_testing/data/groups/get_my_groups.py @@ -175,7 +175,7 @@ }, default=RegisteredResponse( service="groups", - path="/groups/my_groups", + path="/v2/groups/my_groups", json=raw_data, ), ) diff --git a/src/globus_sdk/_testing/data/groups/set_group_policies.py b/src/globus_sdk/_testing/data/groups/set_group_policies.py index 7fc701161..59b8970d5 100644 --- a/src/globus_sdk/_testing/data/groups/set_group_policies.py +++ b/src/globus_sdk/_testing/data/groups/set_group_policies.py @@ -6,7 +6,7 @@ metadata={"group_id": GROUP_ID}, default=RegisteredResponse( service="groups", - path=f"/groups/{GROUP_ID}/policies", + path=f"/v2/groups/{GROUP_ID}/policies", method="PUT", json={ "is_high_assurance": False, diff --git a/src/globus_sdk/_testing/data/transfer/create_endpoint.py b/src/globus_sdk/_testing/data/transfer/create_endpoint.py index c31128696..7a5d9ce8e 100644 --- a/src/globus_sdk/_testing/data/transfer/create_endpoint.py +++ b/src/globus_sdk/_testing/data/transfer/create_endpoint.py @@ -7,7 +7,7 @@ default=RegisteredResponse( service="transfer", method="POST", - path="/endpoint", + path="/v0.10/endpoint", json={ "DATA_TYPE": "endpoint_create_result", "display_name": "my cool endpoint", @@ -16,7 +16,7 @@ "id": ENDPOINT_ID, "message": "Endpoint created successfully", "request_id": "d4MqMwFJ9", - "resource": "/endpoint", + "resource": "/v0.10/endpoint", }, ), ) diff --git a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_list.py b/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_list.py index 1bad1ccc4..9b1897aed 100644 --- a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_list.py +++ b/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_list.py @@ -13,7 +13,7 @@ default=RegisteredResponse( service="transfer", method="GET", - path="/endpoint_manager/task_list", + path="/v0.10/endpoint_manager/task_list", metadata={ "task_id": TASK_ID, "source": SRC_ENDPOINT_ID, diff --git a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_successful_transfers.py b/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_successful_transfers.py index 2610ddb36..b81c7b969 100644 --- a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_successful_transfers.py +++ b/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_successful_transfers.py @@ -7,7 +7,7 @@ default=RegisteredResponse( service="transfer", method="GET", - path=f"/endpoint_manager/task/{TASK_ID}/successful_transfers", + path=f"/v0.10/endpoint_manager/task/{TASK_ID}/successful_transfers", json={ "DATA_TYPE": "successful_transfers", "marker": 0, diff --git a/src/globus_sdk/_testing/data/transfer/get_endpoint.py b/src/globus_sdk/_testing/data/transfer/get_endpoint.py index 68c8fd5a5..b0112132b 100644 --- a/src/globus_sdk/_testing/data/transfer/get_endpoint.py +++ b/src/globus_sdk/_testing/data/transfer/get_endpoint.py @@ -57,7 +57,7 @@ metadata={"endpoint_id": ENDPOINT_ID}, default=RegisteredResponse( service="transfer", - path=f"/endpoint/{ENDPOINT_ID}", + path=f"/v0.10/endpoint/{ENDPOINT_ID}", json=ENDPOINT_DOC, ), ) diff --git a/src/globus_sdk/_testing/data/transfer/get_submission_id.py b/src/globus_sdk/_testing/data/transfer/get_submission_id.py index 820f87858..077dac261 100644 --- a/src/globus_sdk/_testing/data/transfer/get_submission_id.py +++ b/src/globus_sdk/_testing/data/transfer/get_submission_id.py @@ -6,7 +6,7 @@ metadata={"submission_id": SUBMISSION_ID}, default=RegisteredResponse( service="transfer", - path="/submission_id", + path="/v0.10/submission_id", json={"value": SUBMISSION_ID}, ), ) diff --git a/src/globus_sdk/_testing/data/transfer/operation_mkdir.py b/src/globus_sdk/_testing/data/transfer/operation_mkdir.py index 9fa58b2b3..aa4beb5c6 100644 --- a/src/globus_sdk/_testing/data/transfer/operation_mkdir.py +++ b/src/globus_sdk/_testing/data/transfer/operation_mkdir.py @@ -7,13 +7,13 @@ default=RegisteredResponse( service="transfer", method="POST", - path=f"/operation/endpoint/{ENDPOINT_ID}/mkdir", + path=f"/v0.10/operation/endpoint/{ENDPOINT_ID}/mkdir", json={ "DATA_TYPE": "mkdir_result", "code": "DirectoryCreated", "message": "The directory was created successfully", "request_id": "ShbIUzrWT", - "resource": f"/operation/endpoint/{ENDPOINT_ID}/mkdir", + "resource": f"/v0.10/operation/endpoint/{ENDPOINT_ID}/mkdir", }, ), ) diff --git a/src/globus_sdk/_testing/data/transfer/operation_rename.py b/src/globus_sdk/_testing/data/transfer/operation_rename.py index 4c44e4d79..9db38edb4 100644 --- a/src/globus_sdk/_testing/data/transfer/operation_rename.py +++ b/src/globus_sdk/_testing/data/transfer/operation_rename.py @@ -7,13 +7,13 @@ default=RegisteredResponse( service="transfer", method="POST", - path=f"/operation/endpoint/{ENDPOINT_ID}/rename", + path=f"/v0.10/operation/endpoint/{ENDPOINT_ID}/rename", json={ "DATA_TYPE": "result", "code": "FileRenamed", "message": "File or directory renamed successfully", "request_id": "ShbIUzrWT", - "resource": f"/operation/endpoint/{ENDPOINT_ID}/rename", + "resource": f"/v0.10/operation/endpoint/{ENDPOINT_ID}/rename", }, ), ) diff --git a/src/globus_sdk/_testing/data/transfer/operation_stat.py b/src/globus_sdk/_testing/data/transfer/operation_stat.py index 312d5ddda..a59f72a84 100644 --- a/src/globus_sdk/_testing/data/transfer/operation_stat.py +++ b/src/globus_sdk/_testing/data/transfer/operation_stat.py @@ -7,7 +7,7 @@ default=RegisteredResponse( service="transfer", method="GET", - path=f"/operation/endpoint/{ENDPOINT_ID}/stat", + path=f"/v0.10/operation/endpoint/{ENDPOINT_ID}/stat", json={ "DATA_TYPE": "file", "group": "tutorial", @@ -27,25 +27,25 @@ not_found=RegisteredResponse( service="transfer", method="GET", - path=f"/operation/endpoint/{ENDPOINT_ID}/stat", + path=f"/v0.10/operation/endpoint/{ENDPOINT_ID}/stat", status=404, json={ "code": "NotFound", "message": f"Path not found, Error (list)\nEndpoint: Globus Tutorial Collection 1 ({ENDPOINT_ID})\nServer: 100.26.231.26:443\nMessage: No such file or directory\n---\nDetails: Error: '~/foo' not found\\r\\n550-GlobusError: v=1 c=PATH_NOT_FOUND\\r\\n550-GridFTP-Errno: 2\\r\\n550-GridFTP-Reason: System error in stat\\r\\n550-GridFTP-Error-String: No such file or directory\\r\\n550 End.\\r\\n\n", # noqa 501 "request_id": "aaabbbccc", - "resource": f"/operation/endpoint/{ENDPOINT_ID}/stat", + "resource": f"/v0.10/operation/endpoint/{ENDPOINT_ID}/stat", }, ), permission_denied=RegisteredResponse( service="transfer", method="GET", - path=f"/operation/endpoint/{ENDPOINT_ID}/stat", + path=f"/v0.10/operation/endpoint/{ENDPOINT_ID}/stat", status=403, json={ "code": "EndpointPermissionDenied", "message": f"Denied by endpoint, Error (list)\nEndpoint: Globus Tutorial Collection 1 ({ENDPOINT_ID})\nServer: 100.26.231.26:443\nCommand: MLST /foo\nMessage: Fatal FTP Response\n---\nDetails: 500 Command failed : Path not allowed.\\r\\n\n", # noqa 501 "request_id": "aaabbbccc", - "resource": f"/operation/endpoint/{ENDPOINT_ID}/stat", + "resource": f"/v0.10/operation/endpoint/{ENDPOINT_ID}/stat", }, ), ) diff --git a/src/globus_sdk/_testing/data/transfer/set_subscription_id.py b/src/globus_sdk/_testing/data/transfer/set_subscription_id.py index 57cd9b223..d829cea84 100644 --- a/src/globus_sdk/_testing/data/transfer/set_subscription_id.py +++ b/src/globus_sdk/_testing/data/transfer/set_subscription_id.py @@ -11,31 +11,31 @@ default=RegisteredResponse( service="transfer", method="PUT", - path=f"/endpoint/{ENDPOINT_ID}/subscription", + path=f"/v0.10/endpoint/{ENDPOINT_ID}/subscription", json={ "DATA_TYPE": "result", "code": "Updated", "message": "Endpoint updated successfully", "request_id": "dWTZZe17L", - "resource": f"/endpoint/{ENDPOINT_ID}/subscription", + "resource": f"/v0.10/endpoint/{ENDPOINT_ID}/subscription", }, ), not_found=RegisteredResponse( service="transfer", method="PUT", - path=f"/endpoint/{ENDPOINT_ID}/subscription", + path=f"/v0.10/endpoint/{ENDPOINT_ID}/subscription", status=404, json={ "code": "EndpointNotFound", "message": f"No such endpoint '{ENDPOINT_ID}'", "request_id": "BHI2BHt8N", - "resource": f"/endpoint/{ENDPOINT_ID}/subscription", + "resource": f"/v0.10/endpoint/{ENDPOINT_ID}/subscription", }, ), multi_subscriber_cannot_use_default=RegisteredResponse( service="transfer", method="PUT", - path=f"/endpoint/{ENDPOINT_ID}/subscription", + path=f"/v0.10/endpoint/{ENDPOINT_ID}/subscription", status=400, json={ "code": "BadRequest", @@ -45,7 +45,7 @@ f"{SUBSCRIPTION_ID}, {OTHER_SUBSCRIPTION_ID}" ), "request_id": "H1dFNg6QB", - "resource": f"/endpoint/{ENDPOINT_ID}/subscription", + "resource": f"/v0.10/endpoint/{ENDPOINT_ID}/subscription", }, metadata={ "endpoint_id": ENDPOINT_ID, diff --git a/src/globus_sdk/_testing/data/transfer/submit_delete.py b/src/globus_sdk/_testing/data/transfer/submit_delete.py index acc4d4466..5ffa6b7c9 100644 --- a/src/globus_sdk/_testing/data/transfer/submit_delete.py +++ b/src/globus_sdk/_testing/data/transfer/submit_delete.py @@ -7,7 +7,7 @@ default=RegisteredResponse( service="transfer", method="POST", - path="/delete", + path="/v0.10/delete", json={ "DATA_TYPE": "delete_result", "code": "Accepted", @@ -16,12 +16,12 @@ "and queued for execution" ), "request_id": "NS2QXhLZ7", - "resource": "/delete", + "resource": "/v0.10/delete", "submission_id": SUBMISSION_ID, "task_id": TASK_ID, "task_link": { "DATA_TYPE": "link", - "href": f"task/{TASK_ID}?format=json", + "href": f"/v0.10/task/{TASK_ID}?format=json", "rel": "related", "resource": "task", "title": "related task", diff --git a/src/globus_sdk/_testing/data/transfer/submit_transfer.py b/src/globus_sdk/_testing/data/transfer/submit_transfer.py index 00c32ced1..a76d1878b 100644 --- a/src/globus_sdk/_testing/data/transfer/submit_transfer.py +++ b/src/globus_sdk/_testing/data/transfer/submit_transfer.py @@ -7,7 +7,7 @@ default=RegisteredResponse( service="transfer", method="POST", - path="/transfer", + path="/v0.10/transfer", json={ "DATA_TYPE": "transfer_result", "code": "Accepted", @@ -16,12 +16,12 @@ "and queued for execution" ), "request_id": "7HgMVYazI", - "resource": "/transfer", + "resource": "/v0.10/transfer", "submission_id": SUBMISSION_ID, "task_id": TASK_ID, "task_link": { "DATA_TYPE": "link", - "href": f"task/{TASK_ID}?format=json", + "href": f"/v0.10/task/{TASK_ID}?format=json", "rel": "related", "resource": "task", "title": "related task", @@ -31,12 +31,12 @@ failure=RegisteredResponse( service="transfer", method="POST", - path="/transfer", + path="/v0.10/transfer", json={ "code": "ClientError.BadRequest.NoTransferItems", "message": "A transfer requires at least one item", "request_id": "oUAA6Sq2P", - "resource": "/transfer", + "resource": "/v0.10/transfer", }, status=400, metadata={ diff --git a/src/globus_sdk/_testing/data/transfer/task_list.py b/src/globus_sdk/_testing/data/transfer/task_list.py index 26684b8de..0903fe748 100644 --- a/src/globus_sdk/_testing/data/transfer/task_list.py +++ b/src/globus_sdk/_testing/data/transfer/task_list.py @@ -85,7 +85,7 @@ }, default=RegisteredResponse( service="transfer", - path="/task_list", + path="/v0.10/task_list", json=TASK_LIST_DOC, ), ) diff --git a/src/globus_sdk/_testing/data/transfer/update_endpoint.py b/src/globus_sdk/_testing/data/transfer/update_endpoint.py index 767b0b24b..82220d6b7 100644 --- a/src/globus_sdk/_testing/data/transfer/update_endpoint.py +++ b/src/globus_sdk/_testing/data/transfer/update_endpoint.py @@ -7,13 +7,13 @@ default=RegisteredResponse( service="transfer", method="PUT", - path=f"/endpoint/{ENDPOINT_ID}", + path=f"/v0.10/endpoint/{ENDPOINT_ID}", json={ "DATA_TYPE": "result", "code": "Updated", "message": "Endpoint updated successfully", "request_id": "6aZjzldyM", - "resource": f"/endpoint/{ENDPOINT_ID}", + "resource": f"/v0.10/endpoint/{ENDPOINT_ID}", }, ), ) diff --git a/src/globus_sdk/_testing/models.py b/src/globus_sdk/_testing/models.py index 1bfe45938..dec2c6d23 100644 --- a/src/globus_sdk/_testing/models.py +++ b/src/globus_sdk/_testing/models.py @@ -37,18 +37,14 @@ class RegisteredResponse: _url_map = { "auth": "https://auth.globus.org/", "nexus": "https://nexus.api.globusonline.org/", - "transfer": "https://transfer.api.globus.org/v0.10", + "transfer": "https://transfer.api.globus.org/", "search": "https://search.api.globus.org/", - "gcs": "https://abc.xyz.data.globus.org/api", - "groups": "https://groups.api.globus.org/v2/", + "gcs": "https://abc.xyz.data.globus.org/api/", + "groups": "https://groups.api.globus.org/", "timer": "https://timer.automate.globus.org/", "flows": "https://flows.automate.globus.org/", "compute": "https://compute.api.globus.org/", } - _base_path_map = { - "transfer": "/v0.10/", - "groups": "/v2/", - } def __init__( self, @@ -105,13 +101,6 @@ def __init__( self.service = service if service: - # strip base_paths to match the behavior of clients - # this allows a registered response to use a path like `/v2/groups` with - # the GroupsClient, rather than *requiring* that it use `/groups` - base_path = self._base_path_map.get(service) - if base_path and path.startswith(base_path): - path = path[len(base_path) :] - self.full_url = slash_join(self._url_map[service], path) else: self.full_url = path diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 3be1dcb86..7caa2ba6a 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -54,12 +54,6 @@ class BaseClient: # `BaseClient._resolve_base_url` method for more details. base_url: str = "_base" - # path under the client base URL - # NOTE: using this attribute is now considered bad practice for client definitions, - # as it prevents calls to new routes at the root of an API's base_url - # Consider removing in a future release - base_path: str = "/" - #: the class for errors raised by this client on HTTP 4xx and 5xx errors #: this can be set in subclasses, but must always be a subclass of GlobusError error_class: type[exc.GlobusAPIError] = exc.GlobusAPIError @@ -105,8 +99,6 @@ 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) - # append the base_path to the base_url if necessary - self.base_url = utils.slash_join(self.base_url, self.base_path) self.transport = self.transport_class(**(transport_params or {})) log.debug(f"initialized transport of type {type(self.transport)}") @@ -484,11 +476,6 @@ def request( if path.startswith("https://") or path.startswith("http://"): url = path else: - # if passed a path which has a prefix matching the base_path, strip it - # this means that if a client has a base path of `/v1/`, a request for - # `/v1/foo` will hit `/v1/foo` rather than `/v1/v1/foo` - if path.startswith(self.base_path): - path = path[len(self.base_path) :] url = utils.slash_join(self.base_url, urllib.parse.quote(path)) # either use given authorizer or get one from app diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index adce2194b..befa6caf3 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -24,9 +24,6 @@ class GroupsClient(client.BaseClient): .. automethodlist:: globus_sdk.GroupsClient """ - # NOTE: setting base_path is no longer considered good practice - # see the BaseClient source for details - base_path = "/v2/" error_class = GroupsAPIError service_name = "groups" scopes = GroupsScopes @@ -54,7 +51,7 @@ def get_my_groups( :ref: get_my_groups_and_memberships_v2_groups_my_groups_get """ return response.ArrayResponse( - self.get("/groups/my_groups", query_params=query_params) + self.get("/v2/groups/my_groups", query_params=query_params) ) def get_group( @@ -87,7 +84,7 @@ def get_group( query_params = {} if include is not None: query_params["include"] = ",".join(utils.safe_strseq_iter(include)) - return self.get(f"/groups/{group_id}", query_params=query_params) + return self.get(f"/v2/groups/{group_id}", query_params=query_params) def get_group_by_subscription_id( self, subscription_id: UUIDLike @@ -120,7 +117,7 @@ def get_group_by_subscription_id( :service: groups :ref: get_group_by_subscription_id_v2_subscription_info__subscription_id__get """ # noqa: E501 - return self.get(f"/subscription_info/{subscription_id}") + return self.get(f"/v2/subscription_info/{subscription_id}") def delete_group( self, @@ -144,7 +141,7 @@ def delete_group( :service: groups :ref: delete_group_v2_groups__group_id__delete """ - return self.delete(f"/groups/{group_id}", query_params=query_params) + return self.delete(f"/v2/groups/{group_id}", query_params=query_params) def create_group( self, @@ -168,7 +165,7 @@ def create_group( :service: groups :ref: create_group_v2_groups_post """ - return self.post("/groups", data=data, query_params=query_params) + return self.post("/v2/groups", data=data, query_params=query_params) def update_group( self, @@ -194,7 +191,7 @@ def update_group( :service: groups :ref: update_group_v2_groups__group_id__put """ - return self.put(f"/groups/{group_id}", data=data, query_params=query_params) + return self.put(f"/v2/groups/{group_id}", data=data, query_params=query_params) def get_group_policies( self, @@ -218,7 +215,7 @@ def get_group_policies( :service: groups :ref: get_policies_v2_groups__group_id__policies_get """ - return self.get(f"/groups/{group_id}/policies", query_params=query_params) + return self.get(f"/v2/groups/{group_id}/policies", query_params=query_params) def set_group_policies( self, @@ -245,7 +242,7 @@ def set_group_policies( :ref: update_policies_v2_groups__group_id__policies_put """ return self.put( - f"/groups/{group_id}/policies", data=data, query_params=query_params + f"/v2/groups/{group_id}/policies", data=data, query_params=query_params ) def get_identity_preferences( @@ -267,7 +264,7 @@ def get_identity_preferences( :service: groups :ref: get_identity_set_preferences_v2_preferences_get """ - return self.get("/preferences", query_params=query_params) + return self.get("/v2/preferences", query_params=query_params) def set_identity_preferences( self, @@ -299,7 +296,7 @@ def set_identity_preferences( :service: groups :ref: put_identity_set_preferences_v2_preferences_put """ - return self.put("/preferences", data=data, query_params=query_params) + return self.put("/v2/preferences", data=data, query_params=query_params) def get_membership_fields( self, @@ -324,7 +321,7 @@ def get_membership_fields( :ref: get_membership_fields_v2_groups__group_id__membership_fields_get """ # noqa: E501 return self.get( - f"/groups/{group_id}/membership_fields", query_params=query_params + f"/v2/groups/{group_id}/membership_fields", query_params=query_params ) def set_membership_fields( @@ -352,7 +349,7 @@ def set_membership_fields( :ref: put_membership_fields_v2_groups__group_id__membership_fields_put """ # noqa: E501 return self.put( - f"/groups/{group_id}/membership_fields", + f"/v2/groups/{group_id}/membership_fields", data=data, query_params=query_params, ) @@ -393,4 +390,6 @@ def batch_membership_action( :service: groups :ref: group_membership_post_actions_v2_groups__group_id__post """ - return self.post(f"/groups/{group_id}", data=actions, query_params=query_params) + return self.post( + f"/v2/groups/{group_id}", data=actions, query_params=query_params + ) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 026599fc0..a046d1297 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -103,9 +103,6 @@ class TransferClient(client.BaseClient): """ service_name = "transfer" - # NOTE: setting base_path is no longer considered good practice - # see the BaseClient source for details - base_path = "/v0.10/" transport_class: type[TransferRequestsTransport] = TransferRequestsTransport error_class = TransferAPIError scopes = TransferScopes @@ -222,7 +219,7 @@ def get_endpoint( :ref: transfer/endpoints_and_collections/#get_endpoint_or_collection_by_id """ # noqa: E501 log.debug(f"TransferClient.get_endpoint({endpoint_id})") - return self.get(f"endpoint/{endpoint_id}", query_params=query_params) + return self.get(f"/v0.10/endpoint/{endpoint_id}", query_params=query_params) def update_endpoint( self, @@ -270,7 +267,9 @@ def update_endpoint( data["myproxy_server"] = None log.debug(f"TransferClient.update_endpoint({endpoint_id}, ...)") - return self.put(f"endpoint/{endpoint_id}", data=data, query_params=query_params) + return self.put( + f"/v0.10/endpoint/{endpoint_id}", data=data, query_params=query_params + ) def set_subscription_id( self, @@ -327,7 +326,7 @@ def set_subscription_id( :ref: transfer/gcp_management/#associate_collection_subscription """ # noqa: E501 return self.put( - f"/endpoint/{collection_id}/subscription", + f"/v0.10/endpoint/{collection_id}/subscription", data={"subscription_id": subscription_id}, ) @@ -348,7 +347,7 @@ def create_endpoint(self, data: dict[str, t.Any]) -> response.GlobusHTTPResponse ) log.debug("TransferClient.create_endpoint(...)") - return self.post("endpoint", data=data) + return self.post("/v0.10/endpoint", data=data) def delete_endpoint(self, endpoint_id: UUIDLike) -> response.GlobusHTTPResponse: """ @@ -371,7 +370,7 @@ def delete_endpoint(self, endpoint_id: UUIDLike) -> response.GlobusHTTPResponse: :ref: transfer/gcp_management/#delete_collection_by_id """ log.debug(f"TransferClient.delete_endpoint({endpoint_id})") - return self.delete(f"endpoint/{endpoint_id}") + return self.delete(f"/v0.10/endpoint/{endpoint_id}") @paging.has_paginator( paging.HasNextPaginator, @@ -482,7 +481,7 @@ def endpoint_search( query_params["offset"] = offset log.debug(f"TransferClient.endpoint_search({query_params})") return IterableTransferResponse( - self.get("endpoint_search", query_params=query_params) + self.get("/v0.10/endpoint_search", query_params=query_params) ) def endpoint_autoactivate( @@ -511,7 +510,7 @@ def endpoint_autoactivate( query_params["if_expires_in"] = if_expires_in log.debug(f"TransferClient.endpoint_autoactivate({endpoint_id})") return self.post( - f"endpoint/{endpoint_id}/autoactivate", query_params=query_params + f"/v0.10/endpoint/{endpoint_id}/autoactivate", query_params=query_params ) def endpoint_deactivate( @@ -532,7 +531,7 @@ def endpoint_deactivate( """ log.debug(f"TransferClient.endpoint_deactivate({endpoint_id})") return self.post( - f"endpoint/{endpoint_id}/deactivate", query_params=query_params + f"/v0.10/endpoint/{endpoint_id}/deactivate", query_params=query_params ) def endpoint_activate( @@ -558,7 +557,7 @@ def endpoint_activate( """ log.debug(f"TransferClient.endpoint_activate({endpoint_id})") return self.post( - f"endpoint/{endpoint_id}/activate", + f"/v0.10/endpoint/{endpoint_id}/activate", data=requirements_data, query_params=query_params, ) @@ -582,7 +581,7 @@ def endpoint_get_activation_requirements( """ return ActivationRequirementsResponse( self.get( - f"endpoint/{endpoint_id}/activation_requirements", + f"/v0.10/endpoint/{endpoint_id}/activation_requirements", query_params=query_params, ) ) @@ -610,7 +609,7 @@ def my_effective_pause_rule_list( log.debug(f"TransferClient.my_effective_pause_rule_list({endpoint_id}, ...)") return IterableTransferResponse( self.get( - f"endpoint/{endpoint_id}/my_effective_pause_rule_list", + f"/v0.10/endpoint/{endpoint_id}/my_effective_pause_rule_list", query_params=query_params, ) ) @@ -642,7 +641,7 @@ def my_shared_endpoint_list( log.debug(f"TransferClient.my_shared_endpoint_list({endpoint_id}, ...)") return IterableTransferResponse( self.get( - f"endpoint/{endpoint_id}/my_shared_endpoint_list", + f"/v0.10/endpoint/{endpoint_id}/my_shared_endpoint_list", query_params=query_params, ) ) @@ -687,7 +686,7 @@ def get_shared_endpoint_list( query_params["next_token"] = next_token return IterableTransferResponse( self.get( - f"endpoint/{endpoint_id}/shared_endpoint_list", + f"/v0.10/endpoint/{endpoint_id}/shared_endpoint_list", query_params=query_params, ), iter_key="shared_endpoints", @@ -725,7 +724,7 @@ def create_shared_endpoint( :ref: transfer/gcp_management/#create_guest_collection """ log.debug("TransferClient.create_shared_endpoint(...)") - return self.post("shared_endpoint", data=data) + return self.post("/v0.10/shared_endpoint", data=data) # Endpoint servers @@ -751,7 +750,9 @@ def endpoint_server_list( """ # noqa: E501 log.debug(f"TransferClient.endpoint_server_list({endpoint_id}, ...)") return IterableTransferResponse( - self.get(f"endpoint/{endpoint_id}/server_list", query_params=query_params) + self.get( + f"/v0.10/endpoint/{endpoint_id}/server_list", query_params=query_params + ) ) def get_endpoint_server( @@ -779,7 +780,8 @@ def get_endpoint_server( "TransferClient.get_endpoint_server(%s, %s, ...)", endpoint_id, server_id ) return self.get( - f"endpoint/{endpoint_id}/server/{server_id}", query_params=query_params + f"/v0.10/endpoint/{endpoint_id}/server/{server_id}", + query_params=query_params, ) def add_endpoint_server( @@ -795,7 +797,7 @@ def add_endpoint_server( :param server_data: Fields for the new server, as a server document """ log.debug(f"TransferClient.add_endpoint_server({endpoint_id}, ...)") - return self.post(f"endpoint/{endpoint_id}/server", data=server_data) + return self.post(f"/v0.10/endpoint/{endpoint_id}/server", data=server_data) def update_endpoint_server( self, @@ -818,7 +820,9 @@ def update_endpoint_server( endpoint_id, server_id, ) - return self.put(f"endpoint/{endpoint_id}/server/{server_id}", data=server_data) + return self.put( + f"/v0.10/endpoint/{endpoint_id}/server/{server_id}", data=server_data + ) def delete_endpoint_server( self, endpoint_id: UUIDLike, server_id: IntLike @@ -835,7 +839,7 @@ def delete_endpoint_server( log.debug( "TransferClient.delete_endpoint_server(%s, %s)", endpoint_id, server_id ) - return self.delete(f"endpoint/{endpoint_id}/server/{server_id}") + return self.delete(f"/v0.10/endpoint/{endpoint_id}/server/{server_id}") # # Roles @@ -863,7 +867,9 @@ def endpoint_role_list( """ log.debug(f"TransferClient.endpoint_role_list({endpoint_id}, ...)") return IterableTransferResponse( - self.get(f"endpoint/{endpoint_id}/role_list", query_params=query_params) + self.get( + f"/v0.10/endpoint/{endpoint_id}/role_list", query_params=query_params + ) ) def add_endpoint_role( @@ -883,7 +889,7 @@ def add_endpoint_role( :ref: transfer/roles/#create_role """ log.debug(f"TransferClient.add_endpoint_role({endpoint_id}, ...)") - return self.post(f"endpoint/{endpoint_id}/role", data=role_data) + return self.post(f"/v0.10/endpoint/{endpoint_id}/role", data=role_data) def get_endpoint_role( self, @@ -908,7 +914,7 @@ def get_endpoint_role( """ log.debug(f"TransferClient.get_endpoint_role({endpoint_id}, {role_id}, ...)") return self.get( - f"endpoint/{endpoint_id}/role/{role_id}", query_params=query_params + f"/v0.10/endpoint/{endpoint_id}/role/{role_id}", query_params=query_params ) def delete_endpoint_role( @@ -928,7 +934,7 @@ def delete_endpoint_role( :ref: transfer/roles/#delete_role_by_id """ log.debug(f"TransferClient.delete_endpoint_role({endpoint_id}, {role_id})") - return self.delete(f"endpoint/{endpoint_id}/role/{role_id}") + return self.delete(f"/v0.10/endpoint/{endpoint_id}/role/{role_id}") # # ACLs @@ -955,7 +961,9 @@ def endpoint_acl_list( """ log.debug(f"TransferClient.endpoint_acl_list({endpoint_id}, ...)") return IterableTransferResponse( - self.get(f"endpoint/{endpoint_id}/access_list", query_params=query_params) + self.get( + f"/v0.10/endpoint/{endpoint_id}/access_list", query_params=query_params + ) ) def get_endpoint_acl_rule( @@ -983,7 +991,7 @@ def get_endpoint_acl_rule( "TransferClient.get_endpoint_acl_rule(%s, %s, ...)", endpoint_id, rule_id ) return self.get( - f"endpoint/{endpoint_id}/access/{rule_id}", query_params=query_params + f"/v0.10/endpoint/{endpoint_id}/access/{rule_id}", query_params=query_params ) def add_endpoint_acl_rule( @@ -1021,7 +1029,7 @@ def add_endpoint_acl_rule( :ref: transfer/acl/#rest_access_create """ log.debug(f"TransferClient.add_endpoint_acl_rule({endpoint_id}, ...)") - return self.post(f"endpoint/{endpoint_id}/access", data=rule_data) + return self.post(f"/v0.10/endpoint/{endpoint_id}/access", data=rule_data) def update_endpoint_acl_rule( self, @@ -1048,7 +1056,9 @@ def update_endpoint_acl_rule( endpoint_id, rule_id, ) - return self.put(f"endpoint/{endpoint_id}/access/{rule_id}", data=rule_data) + return self.put( + f"/v0.10/endpoint/{endpoint_id}/access/{rule_id}", data=rule_data + ) def delete_endpoint_acl_rule( self, endpoint_id: UUIDLike, rule_id: str @@ -1069,7 +1079,7 @@ def delete_endpoint_acl_rule( log.debug( "TransferClient.delete_endpoint_acl_rule(%s, %s)", endpoint_id, rule_id ) - return self.delete(f"endpoint/{endpoint_id}/access/{rule_id}") + return self.delete(f"/v0.10/endpoint/{endpoint_id}/access/{rule_id}") # # Bookmarks @@ -1092,7 +1102,7 @@ def bookmark_list( """ log.debug(f"TransferClient.bookmark_list({query_params})") return IterableTransferResponse( - self.get("bookmark_list", query_params=query_params) + self.get("/v0.10/bookmark_list", query_params=query_params) ) def create_bookmark( @@ -1111,7 +1121,7 @@ def create_bookmark( :ref: transfer/collection_bookmarks/#create_bookmark """ log.debug(f"TransferClient.create_bookmark({bookmark_data})") - return self.post("bookmark", data=bookmark_data) + return self.post("/v0.10/bookmark", data=bookmark_data) def get_bookmark( self, @@ -1133,7 +1143,7 @@ def get_bookmark( :ref: transfer/collection_bookmarks/#get_bookmark_by_id """ log.debug(f"TransferClient.get_bookmark({bookmark_id})") - return self.get(f"bookmark/{bookmark_id}", query_params=query_params) + return self.get(f"/v0.10/bookmark/{bookmark_id}", query_params=query_params) def update_bookmark( self, bookmark_id: UUIDLike, bookmark_data: dict[str, t.Any] @@ -1152,7 +1162,7 @@ def update_bookmark( :ref: transfer/collection_bookmarks/#update_bookmark """ log.debug(f"TransferClient.update_bookmark({bookmark_id})") - return self.put(f"bookmark/{bookmark_id}", data=bookmark_data) + return self.put(f"/v0.10/bookmark/{bookmark_id}", data=bookmark_data) def delete_bookmark(self, bookmark_id: UUIDLike) -> response.GlobusHTTPResponse: """ @@ -1168,7 +1178,7 @@ def delete_bookmark(self, bookmark_id: UUIDLike) -> response.GlobusHTTPResponse: :ref: transfer/collection_bookmarks/#delete_bookmark_by_id """ log.debug(f"TransferClient.delete_bookmark({bookmark_id})") - return self.delete(f"bookmark/{bookmark_id}") + return self.delete(f"/v0.10/bookmark/{bookmark_id}") # # Synchronous Filesys Operations @@ -1283,7 +1293,9 @@ def operation_ls( log.debug(f"TransferClient.operation_ls({endpoint_id}, {query_params})") return IterableTransferResponse( - self.get(f"operation/endpoint/{endpoint_id}/ls", query_params=query_params) + self.get( + f"/v0.10/operation/endpoint/{endpoint_id}/ls", query_params=query_params + ) ) def operation_mkdir( @@ -1327,7 +1339,7 @@ def operation_mkdir( if local_user is not None: json_body["local_user"] = local_user return self.post( - f"operation/endpoint/{endpoint_id}/mkdir", + f"/v0.10/operation/endpoint/{endpoint_id}/mkdir", data=json_body, query_params=query_params, ) @@ -1379,7 +1391,7 @@ def operation_rename( if local_user is not None: json_body["local_user"] = local_user return self.post( - f"operation/endpoint/{endpoint_id}/rename", + f"/v0.10/operation/endpoint/{endpoint_id}/rename", data=json_body, query_params=query_params, ) @@ -1429,7 +1441,7 @@ def operation_stat( log.debug(f"TransferClient.operation_stat({endpoint_id}, {query_params})") return self.get( - f"operation/endpoint/{endpoint_id}/stat", query_params=query_params + f"/v0.10/operation/endpoint/{endpoint_id}/stat", query_params=query_params ) def operation_symlink( @@ -1465,7 +1477,7 @@ def operation_symlink( "path": path, } return self.post( - f"operation/endpoint/{endpoint_id}/symlink", + f"/v0.10/operation/endpoint/{endpoint_id}/symlink", data=data, query_params=query_params, ) @@ -1502,7 +1514,7 @@ def get_submission_id( .. expandtestfixture:: transfer.get_submission_id """ log.debug(f"TransferClient.get_submission_id({query_params})") - return self.get("submission_id", query_params=query_params) + return self.get("/v0.10/submission_id", query_params=query_params) def submit_transfer( self, data: dict[str, t.Any] | TransferData @@ -1551,7 +1563,7 @@ def submit_transfer( if "submission_id" not in data: log.debug("submit_transfer autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] - return self.post("/transfer", data=data) + return self.post("/v0.10/transfer", data=data) def submit_delete( self, data: dict[str, t.Any] | DeleteData @@ -1594,7 +1606,7 @@ def submit_delete( if "submission_id" not in data: log.debug("submit_delete autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] - return self.post("/delete", data=data) + return self.post("/v0.10/delete", data=data) # # Task inspection and management @@ -1704,7 +1716,7 @@ def task_list( if filter is not None: query_params["filter"] = _format_filter_item(filter) return IterableTransferResponse( - self.get("task_list", query_params=query_params) + self.get("/v0.10/task_list", query_params=query_params) ) @paging.has_paginator( @@ -1762,7 +1774,7 @@ def task_event_list( if offset is not None: query_params["offset"] = offset return IterableTransferResponse( - self.get(f"task/{task_id}/event_list", query_params=query_params) + self.get(f"/v0.10/task/{task_id}/event_list", query_params=query_params) ) def get_task( @@ -1785,7 +1797,7 @@ def get_task( :ref: transfer/task/#get_task_by_id """ log.debug(f"TransferClient.get_task({task_id}, ...)") - return self.get(f"task/{task_id}", query_params=query_params) + return self.get(f"/v0.10/task/{task_id}", query_params=query_params) def update_task( self, @@ -1812,7 +1824,7 @@ def update_task( :ref: transfer/task/#update_task_by_id """ log.debug(f"TransferClient.update_task({task_id}, ...)") - return self.put(f"task/{task_id}", data=data, query_params=query_params) + return self.put(f"/v0.10/task/{task_id}", data=data, query_params=query_params) def cancel_task(self, task_id: UUIDLike) -> response.GlobusHTTPResponse: """ @@ -1830,7 +1842,7 @@ def cancel_task(self, task_id: UUIDLike) -> response.GlobusHTTPResponse: :ref: transfer/task/#cancel_task_by_id """ log.debug(f"TransferClient.cancel_task({task_id})") - return self.post(f"task/{task_id}/cancel") + return self.post(f"/v0.10/task/{task_id}/cancel") def task_wait( self, task_id: UUIDLike, *, timeout: int = 10, polling_interval: int = 10 @@ -1957,7 +1969,7 @@ def task_pause_info( :ref: transfer/task/#get_task_pause_info """ log.debug(f"TransferClient.task_pause_info({task_id}, ...)") - return self.get(f"task/{task_id}/pause_info", query_params=query_params) + return self.get(f"/v0.10/task/{task_id}/pause_info", query_params=query_params) @paging.has_paginator( paging.NullableMarkerPaginator, items_key="DATA", marker_key="next_marker" @@ -2013,7 +2025,9 @@ def task_successful_transfers( if marker is not None: query_params["marker"] = marker return IterableTransferResponse( - self.get(f"task/{task_id}/successful_transfers", query_params=query_params) + self.get( + f"/v0.10/task/{task_id}/successful_transfers", query_params=query_params + ) ) @paging.has_paginator( @@ -2064,7 +2078,7 @@ def task_skipped_errors( if marker is not None: query_params["marker"] = marker return IterableTransferResponse( - self.get(f"task/{task_id}/skipped_errors", query_params=query_params) + self.get(f"/v0.10/task/{task_id}/skipped_errors", query_params=query_params) ) # @@ -2092,7 +2106,9 @@ def endpoint_manager_monitored_endpoints( f"TransferClient.endpoint_manager_monitored_endpoints({query_params})" ) return IterableTransferResponse( - self.get("endpoint_manager/monitored_endpoints", query_params=query_params) + self.get( + "/v0.10/endpoint_manager/monitored_endpoints", query_params=query_params + ) ) def endpoint_manager_hosted_endpoint_list( @@ -2121,7 +2137,7 @@ def endpoint_manager_hosted_endpoint_list( ) return IterableTransferResponse( self.get( - f"endpoint_manager/endpoint/{endpoint_id}/hosted_endpoint_list", + f"/v0.10/endpoint_manager/endpoint/{endpoint_id}/hosted_endpoint_list", query_params=query_params, ) ) @@ -2149,7 +2165,7 @@ def endpoint_manager_get_endpoint( """ # noqa: E501 log.debug(f"TransferClient.endpoint_manager_get_endpoint({endpoint_id})") return self.get( - f"endpoint_manager/endpoint/{endpoint_id}", query_params=query_params + f"/v0.10/endpoint_manager/endpoint/{endpoint_id}", query_params=query_params ) def endpoint_manager_acl_list( @@ -2178,7 +2194,7 @@ def endpoint_manager_acl_list( ) return IterableTransferResponse( self.get( - f"endpoint_manager/endpoint/{endpoint_id}/access_list", + f"/v0.10/endpoint_manager/endpoint/{endpoint_id}/access_list", query_params=query_params, ) ) @@ -2342,7 +2358,7 @@ def endpoint_manager_task_list( if last_key is not None: query_params["last_key"] = last_key return IterableTransferResponse( - self.get("endpoint_manager/task_list", query_params=query_params) + self.get("/v0.10/endpoint_manager/task_list", query_params=query_params) ) def endpoint_manager_get_task( @@ -2368,7 +2384,9 @@ def endpoint_manager_get_task( :ref: transfer/advanced_collection_management/#get_task """ log.debug(f"TransferClient.endpoint_manager_get_task({task_id}, ...)") - return self.get(f"endpoint_manager/task/{task_id}", query_params=query_params) + return self.get( + f"/v0.10/endpoint_manager/task/{task_id}", query_params=query_params + ) @paging.has_paginator( paging.LimitOffsetTotalPaginator, @@ -2423,7 +2441,8 @@ def endpoint_manager_task_event_list( query_params["filter_is_error"] = 1 if filter_is_error else 0 return IterableTransferResponse( self.get( - f"endpoint_manager/task/{task_id}/event_list", query_params=query_params + f"/v0.10/endpoint_manager/task/{task_id}/event_list", + query_params=query_params, ) ) @@ -2451,7 +2470,8 @@ def endpoint_manager_task_pause_info( """ # noqa: E501 log.debug(f"TransferClient.endpoint_manager_task_pause_info({task_id}, ...)") return self.get( - f"endpoint_manager/task/{task_id}/pause_info", query_params=query_params + f"/v0.10/endpoint_manager/task/{task_id}/pause_info", + query_params=query_params, ) @paging.has_paginator( @@ -2495,7 +2515,7 @@ def endpoint_manager_task_successful_transfers( query_params["marker"] = marker return IterableTransferResponse( self.get( - f"endpoint_manager/task/{task_id}/successful_transfers", + f"/v0.10/endpoint_manager/task/{task_id}/successful_transfers", query_params=query_params, ) ) @@ -2540,7 +2560,7 @@ def endpoint_manager_task_skipped_errors( query_params["marker"] = marker return IterableTransferResponse( self.get( - f"endpoint_manager/task/{task_id}/skipped_errors", + f"/v0.10/endpoint_manager/task/{task_id}/skipped_errors", query_params=query_params, ) ) @@ -2575,7 +2595,7 @@ def endpoint_manager_cancel_tasks( ) data = {"message": message, "task_id_list": str_task_ids} return self.post( - "endpoint_manager/admin_cancel", data=data, query_params=query_params + "/v0.10/endpoint_manager/admin_cancel", data=data, query_params=query_params ) def endpoint_manager_cancel_status( @@ -2602,7 +2622,7 @@ def endpoint_manager_cancel_status( """ # noqa: E501 log.debug(f"TransferClient.endpoint_manager_cancel_status({admin_cancel_id})") return self.get( - f"endpoint_manager/admin_cancel/{admin_cancel_id}", + f"/v0.10/endpoint_manager/admin_cancel/{admin_cancel_id}", query_params=query_params, ) @@ -2636,7 +2656,7 @@ def endpoint_manager_pause_tasks( ) data = {"message": message, "task_id_list": str_task_ids} return self.post( - "endpoint_manager/admin_pause", data=data, query_params=query_params + "/v0.10/endpoint_manager/admin_pause", data=data, query_params=query_params ) def endpoint_manager_resume_tasks( @@ -2665,7 +2685,7 @@ def endpoint_manager_resume_tasks( log.debug(f"TransferClient.endpoint_manager_resume_tasks({str_task_ids})") data = {"task_id_list": str_task_ids} return self.post( - "endpoint_manager/admin_resume", data=data, query_params=query_params + "/v0.10/endpoint_manager/admin_resume", data=data, query_params=query_params ) # @@ -2702,7 +2722,9 @@ def endpoint_manager_pause_rule_list( if filter_endpoint is not None: query_params["filter_endpoint"] = filter_endpoint return IterableTransferResponse( - self.get("endpoint_manager/pause_rule_list", query_params=query_params) + self.get( + "/v0.10/endpoint_manager/pause_rule_list", query_params=query_params + ) ) def endpoint_manager_create_pause_rule( @@ -2739,7 +2761,7 @@ def endpoint_manager_create_pause_rule( :ref: transfer/advanced_collection_management/#create_pause_rule """ log.debug("TransferClient.endpoint_manager_create_pause_rule(...)") - return self.post("endpoint_manager/pause_rule", data=data) + return self.post("/v0.10/endpoint_manager/pause_rule", data=data) def endpoint_manager_get_pause_rule( self, @@ -2765,7 +2787,8 @@ def endpoint_manager_get_pause_rule( """ log.debug(f"TransferClient.endpoint_manager_get_pause_rule({pause_rule_id})") return self.get( - f"endpoint_manager/pause_rule/{pause_rule_id}", query_params=query_params + f"/v0.10/endpoint_manager/pause_rule/{pause_rule_id}", + query_params=query_params, ) def endpoint_manager_update_pause_rule( @@ -2803,7 +2826,9 @@ def endpoint_manager_update_pause_rule( :ref: transfer/advanced_collection_management/#update_pause_rule """ log.debug(f"TransferClient.endpoint_manager_update_pause_rule({pause_rule_id})") - return self.put(f"endpoint_manager/pause_rule/{pause_rule_id}", data=data) + return self.put( + f"/v0.10/endpoint_manager/pause_rule/{pause_rule_id}", data=data + ) def endpoint_manager_delete_pause_rule( self, @@ -2830,5 +2855,6 @@ def endpoint_manager_delete_pause_rule( """ log.debug(f"TransferClient.endpoint_manager_delete_pause_rule({pause_rule_id})") return self.delete( - f"endpoint_manager/pause_rule/{pause_rule_id}", query_params=query_params + f"/v0.10/endpoint_manager/pause_rule/{pause_rule_id}", + query_params=query_params, ) diff --git a/tests/functional/services/groups/test_group_memberships.py b/tests/functional/services/groups/test_group_memberships.py index dfc3a695f..eb8275ffb 100644 --- a/tests/functional/services/groups/test_group_memberships.py +++ b/tests/functional/services/groups/test_group_memberships.py @@ -63,7 +63,7 @@ def test_batch_action_payload(groups_client, role): group_id = str(uuid.uuid1()) load_response( RegisteredResponse( - service="groups", method="POST", path=f"/groups/{group_id}", json={} + service="groups", method="POST", path=f"/v2/groups/{group_id}", json={} ) ) rolestr = role if isinstance(role, str) else role.value diff --git a/tests/functional/services/transfer/test_operation_ls.py b/tests/functional/services/transfer/test_operation_ls.py index f4c8438d6..68f3107e3 100644 --- a/tests/functional/services/transfer/test_operation_ls.py +++ b/tests/functional/services/transfer/test_operation_ls.py @@ -40,7 +40,7 @@ def _setup_ls_response(): load_response( RegisteredResponse( service="transfer", - path=f"/operation/endpoint/{GO_EP1_ID}/ls", + path=f"/v0.10/operation/endpoint/{GO_EP1_ID}/ls", json=_mk_ls_data(), ), ) diff --git a/tests/functional/services/transfer/test_operation_symlink.py b/tests/functional/services/transfer/test_operation_symlink.py index 01eaaf03e..de85f72d1 100644 --- a/tests/functional/services/transfer/test_operation_symlink.py +++ b/tests/functional/services/transfer/test_operation_symlink.py @@ -16,7 +16,7 @@ def _setup_symlink_response(symlink_endpoint_id): RegisteredResponse( service="transfer", method="POST", - path=f"/operation/endpoint/{symlink_endpoint_id}/symlink", + path=f"/v0.10/operation/endpoint/{symlink_endpoint_id}/symlink", json={}, ).add() diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index b955b24d5..182592670 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -21,7 +21,6 @@ def auth_client(): @pytest.fixture def base_client_class(no_retry_transport): class CustomClient(globus_sdk.BaseClient): - base_path = "/v0.10/" service_name = "transfer" transport_class = no_retry_transport scopes = TransferScopes @@ -47,16 +46,13 @@ def test_cannot_instantiate_plain_base_client(): def test_can_instantiate_base_client_with_explicit_url(): - # note how a trailing slash is added due to the default - # base_path of '/' - # this may change in a future major version, to preserve the base_url exactly client = globus_sdk.BaseClient(base_url="https://example.org") - assert client.base_url == "https://example.org/" + assert client.base_url == "https://example.org" def test_can_instantiate_with_base_url_class_attribute(): class MyCoolClient(globus_sdk.BaseClient): - base_url = "https://example.org" + base_url = "https://example.org/" client = MyCoolClient() assert client.base_url == "https://example.org/" @@ -77,8 +73,8 @@ class OnlyServiceClient(globus_sdk.BaseClient): service_name = "service-name" # All 3 are set - assert BothAttributesClient(base_url="init-base").base_url == "init-base/" - assert BothAttributesClient().base_url == "class-base/" + assert BothAttributesClient(base_url="init-base").base_url == "init-base" + assert BothAttributesClient().base_url == "class-base" assert OnlyServiceClient().base_url == "https://service-name.api.globus.org/" @@ -142,7 +138,7 @@ def test_http_methods(method, allows_body, base_client): """ methodname = method.upper() resolved_method = getattr(base_client, method) - path = "/madeuppath/objectname" + path = "/v0.10/madeuppath/objectname" RegisteredResponse( service="transfer", path=path, method=methodname, json={"x": "y"} ).add() @@ -195,8 +191,10 @@ def test_http_methods(method, allows_body, base_client): def test_handle_url_unsafe_chars(base_client): # make sure this path (escaped) and the request path (unescaped) match - RegisteredResponse(service="transfer", path="/foo/foo%20bar", json={"x": "y"}).add() - res = base_client.get("foo/foo bar") + RegisteredResponse( + service="transfer", path="/v0.10/foo/foo%20bar", json={"x": "y"} + ).add() + res = base_client.get("/v0.10/foo/foo bar") assert "x" in res assert res["x"] == "y" @@ -211,33 +209,6 @@ def test_access_resource_server_property_via_class(base_client_class): assert base_client_class.resource_server == TransferScopes.resource_server -@pytest.mark.parametrize("leading_slash", (True, False)) -@pytest.mark.parametrize("test_fixture_uses_base_path", (True, False)) -def test_base_path_matching_prefix( - base_client, leading_slash, test_fixture_uses_base_path -): - # self-check/sanity check - base_path = base_client.base_path - assert base_path == "/v0.10/" - - # construct the path and confirm it (sanity check) - req_path = f"{base_client.base_path}foo" - if not leading_slash: - req_path.lstrip("/") - - # register a response under the target path - # this is parametrized so that we are also testing the matching of our - # test fixtures against the same path construction - test_fixture_path = f"{base_path}foo" if test_fixture_uses_base_path else "foo" - RegisteredResponse( - service="transfer", path=test_fixture_path, json={"x": "y"} - ).add() - - # confirm that a "GET" works - res = base_client.get(req_path) - assert res["x"] == "y" - - def test_app_integration(base_client_class): def _reraise_token_error(_: GlobusApp, error: TokenValidationError): raise error @@ -356,7 +327,6 @@ def test_cannot_attach_app_when_authorizer_was_provided(base_client_class): def test_cannot_attach_app_when_resource_server_is_not_resolvable(): class CustomClient(globus_sdk.BaseClient): - base_path = "/v0.10/" service_name = "transfer" default_scope_requirements = [Scope(TransferScopes.all)] diff --git a/tests/unit/test_gcs_client.py b/tests/unit/test_gcs_client.py index d4cad03a6..e48976592 100644 --- a/tests/unit/test_gcs_client.py +++ b/tests/unit/test_gcs_client.py @@ -6,25 +6,23 @@ def test_client_address_handling(): # variants of the same location c1 = GCSClient("foo.data.globus.org") c2 = GCSClient("https://foo.data.globus.org") - c3 = GCSClient("https://foo.data.globus.org/api") - c4 = GCSClient("https://foo.data.globus.org/api/") + c3 = GCSClient("https://foo.data.globus.org/api/") # explicit subpath of /api/ - c5 = GCSClient("https://foo.data.globus.org/api/bar") + c4 = GCSClient("https://foo.data.globus.org/api/bar") # explicit construction can point at the root (rather than /api/) - c6 = GCSClient("foo.data.globus.org") - c6.base_url = "https://foo.data.globus.org/" + c5 = GCSClient("foo.data.globus.org") + c5.base_url = "https://foo.data.globus.org/" - # 1, 2, 3, and 4 are all the same + # 1, 2, and 3 are all the same assert c1.base_url == c2.base_url assert c1.base_url == c3.base_url - assert c1.base_url == c4.base_url - # 5 and 6 are different from the rest + # 4 and 5 are different from the rest + assert c4.base_url != c1.base_url assert c5.base_url != c1.base_url - assert c6.base_url != c1.base_url - # 6 is the root of 1 - assert c1.base_url.startswith(c6.base_url) + # 5 is the root of 1 + assert c1.base_url.startswith(c5.base_url) def test_gcs_client_resource_server_and_endpoint_client_id(): From 1a05bacaa8def10983fb79e736502734bba3334c Mon Sep 17 00:00:00 2001 From: Jason Alt Date: Tue, 13 May 2025 18:11:04 +0000 Subject: [PATCH 005/176] Updated CollectionDocument with MissingType --- changelog.d/20250513_182417_jasonalt.rst | 53 ++++ .../services/gcs/data/collection.py | 293 ++++++++++-------- tests/unit/helpers/gcs/test_collections.py | 130 +++++++- 3 files changed, 336 insertions(+), 140 deletions(-) create mode 100644 changelog.d/20250513_182417_jasonalt.rst diff --git a/changelog.d/20250513_182417_jasonalt.rst b/changelog.d/20250513_182417_jasonalt.rst new file mode 100644 index 000000000..59b66bf72 --- /dev/null +++ b/changelog.d/20250513_182417_jasonalt.rst @@ -0,0 +1,53 @@ +.. +.. A new scriv changelog fragment +.. +.. Uncomment the header that is right (remove the leading dots). +.. +.. Leave the "(:pr:`...`)" text in your change description. +.. GitHub Actions will automatically replace it when the PR is merged. +.. +.. Python Support +.. ~~~~~~~~~~~~~~ +.. +.. - A bullet item for the Python Support category. (:pr:`NUMBER`) +.. +.. Added +.. ~~~~~ +.. +.. - A bullet item for the Added category. (:pr:`NUMBER`) +.. +.. Removed +.. ~~~~~~~ +.. +.. - A bullet item for the Removed category. (:pr:`NUMBER`) +.. +Changed +~~~~~~~ + +- Updated MappedCollectionDoc and GuestCollectionDoc with MissingType. (:pr:`NUMBER`) + +.. Deprecated +.. ~~~~~~~~~~ +.. +.. - A bullet item for the Deprecated category. (:pr:`NUMBER`) +.. +.. Fixed +.. ~~~~~ +.. +.. - A bullet item for the Fixed category. (:pr:`NUMBER`) +.. +.. Documentation +.. ~~~~~~~~~~~~~ +.. +.. - A bullet item for the Documentation category. (:pr:`NUMBER`) +.. +.. Security +.. ~~~~~~~~ +.. +.. - A bullet item for the Security category. (:pr:`NUMBER`) +.. +.. Development +.. ~~~~~~~~~~~ +.. +.. - A bullet item for the Development category. (:pr:`NUMBER`) +.. diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index fc509fbb1..98a0d0d0f 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -5,6 +5,7 @@ from globus_sdk import utils from globus_sdk._types import UUIDLike +from globus_sdk.utils import MISSING, MissingType from ._common import ( DatatypeCallback, @@ -125,59 +126,58 @@ def __init__( self, *, # data_type - data_type: str | None = None, + data_type: str | MissingType = MISSING, # strs - collection_base_path: str | None = None, - contact_email: str | None = None, - contact_info: str | None = None, - default_directory: str | None = None, - department: str | None = None, - description: str | None = None, - display_name: str | None = None, - identity_id: UUIDLike | None = None, - info_link: str | None = None, - organization: str | None = None, - user_message: str | None = None, - user_message_link: str | None = None, + collection_base_path: str | MissingType = MISSING, + contact_email: str | None | MissingType = MISSING, + contact_info: str | None | MissingType = MISSING, + default_directory: str | MissingType = MISSING, + department: str | None | MissingType = MISSING, + description: str | None | MissingType = MISSING, + display_name: str | MissingType = MISSING, + identity_id: UUIDLike | MissingType = MISSING, + info_link: str | None | MissingType = MISSING, + organization: str | MissingType = MISSING, + user_message: str | None | MissingType = MISSING, + user_message_link: str | None | MissingType = MISSING, # str lists - keywords: t.Iterable[str] | None = None, + keywords: t.Iterable[str] | MissingType = MISSING, # bools - disable_verify: bool | None = None, - enable_https: bool | None = None, - force_encryption: bool | None = None, - force_verify: bool | None = None, - public: bool | None = None, + disable_verify: bool | MissingType = MISSING, + enable_https: bool | MissingType = MISSING, + force_encryption: bool | MissingType = MISSING, + force_verify: bool | MissingType = MISSING, + public: bool | MissingType = MISSING, # additional fields - additional_fields: dict[str, t.Any] | None = None, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> None: super().__init__() self["collection_type"] = self.collection_type - self._set_optstrs( - DATA_TYPE=data_type, - collection_base_path=collection_base_path, - contact_email=contact_email, - contact_info=contact_info, - default_directory=default_directory, - department=department, - description=description, - display_name=display_name, - identity_id=identity_id, - info_link=info_link, - organization=organization, - user_message=user_message, - user_message_link=user_message_link, + self["DATA_TYPE"] = data_type + self["collection_base_path"] = collection_base_path + self["contact_email"] = contact_email + self["contact_info"] = contact_info + self["default_directory"] = default_directory + self["department"] = department + self["description"] = description + self["display_name"] = display_name + self["identity_id"] = identity_id + self["info_link"] = info_link + self["organization"] = organization + self["user_message"] = user_message + self["user_message_link"] = user_message_link + self["keywords"] = ( + keywords + if isinstance(keywords, MissingType) + else list(utils.safe_strseq_iter(keywords)) ) - self._set_optstrlists( - keywords=keywords, - ) - self._set_optbools( - disable_verify=disable_verify, - enable_https=enable_https, - force_encryption=force_encryption, - force_verify=force_verify, - public=public, - ) - if additional_fields is not None: + self["disable_verify"] = disable_verify + self["enable_https"] = enable_https + self["force_encryption"] = force_encryption + self["force_verify"] = force_verify + self["public"] = public + + if not isinstance(additional_fields, MissingType): self.update(additional_fields) @property @@ -234,48 +234,48 @@ def __init__( self, *, # data type - data_type: str | None = None, + data_type: str | MissingType = MISSING, # > common args start < # strs - collection_base_path: str | None = None, - contact_email: str | None = None, - contact_info: str | None = None, - default_directory: str | None = None, - department: str | None = None, - description: str | None = None, - display_name: str | None = None, - identity_id: UUIDLike | None = None, - info_link: str | None = None, - organization: str | None = None, - user_message: str | None = None, - user_message_link: str | None = None, + collection_base_path: str | MissingType = MISSING, + contact_email: str | None | MissingType = MISSING, + contact_info: str | None | MissingType = MISSING, + default_directory: str | MissingType = MISSING, + department: str | None | MissingType = MISSING, + description: str | None | MissingType = MISSING, + display_name: str | MissingType = MISSING, + identity_id: UUIDLike | MissingType = MISSING, + info_link: str | None | MissingType = MISSING, + organization: str | MissingType = MISSING, + user_message: str | None | MissingType = MISSING, + user_message_link: str | None | MissingType = MISSING, # str lists - keywords: t.Iterable[str] | None = None, + keywords: t.Iterable[str] | MissingType = MISSING, # bools - disable_verify: bool | None = None, - enable_https: bool | None = None, - force_encryption: bool | None = None, - force_verify: bool | None = None, - public: bool | None = None, + disable_verify: bool | MissingType = MISSING, + enable_https: bool | MissingType = MISSING, + force_encryption: bool | MissingType = MISSING, + force_verify: bool | MissingType = MISSING, + public: bool | MissingType = MISSING, # > common args end < # > specific args start < # strs - domain_name: str | None = None, - guest_auth_policy_id: UUIDLike | None = None, - storage_gateway_id: UUIDLike | None = None, + domain_name: str | MissingType = MISSING, + guest_auth_policy_id: UUIDLike | None | MissingType = MISSING, + storage_gateway_id: UUIDLike | MissingType = MISSING, # str lists - sharing_users_allow: t.Iterable[str] | None = None, - sharing_users_deny: t.Iterable[str] | None = None, - sharing_restrict_paths: dict[str, t.Any] | None = None, + sharing_users_allow: t.Iterable[str] | None | MissingType = MISSING, + sharing_users_deny: t.Iterable[str] | None | MissingType = MISSING, + sharing_restrict_paths: dict[str, t.Any] | None | MissingType = MISSING, # bools - delete_protected: bool | None = None, - allow_guest_collections: bool | None = None, - disable_anonymous_writes: bool | None = None, + delete_protected: bool | MissingType = MISSING, + allow_guest_collections: bool | MissingType = MISSING, + disable_anonymous_writes: bool | MissingType = MISSING, # dicts - policies: CollectionPolicies | dict[str, t.Any] | None = None, + policies: CollectionPolicies | dict[str, t.Any] | MissingType = MISSING, # > specific args end < # additional fields - additional_fields: dict[str, t.Any] | None = None, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> None: super().__init__( # data type @@ -304,22 +304,27 @@ def __init__( # additional fields additional_fields=additional_fields, ) - self._set_optstrs( - domain_name=domain_name, - guest_auth_policy_id=guest_auth_policy_id, - storage_gateway_id=storage_gateway_id, - ) - self._set_optstrlists( - sharing_users_allow=sharing_users_allow, - sharing_users_deny=sharing_users_deny, + + self["domain_name"] = domain_name + self["guest_auth_policy_id"] = guest_auth_policy_id + self["storage_gateway_id"] = storage_gateway_id + + self["sharing_users_allow"] = ( + sharing_users_allow + if isinstance(sharing_users_allow, (MissingType, type(None))) + else list(utils.safe_strseq_iter(sharing_users_allow)) ) - self._set_optbools( - delete_protected=delete_protected, - allow_guest_collections=allow_guest_collections, - disable_anonymous_writes=disable_anonymous_writes, + self["sharing_users_deny"] = ( + sharing_users_deny + if isinstance(sharing_users_deny, (MissingType, type(None))) + else list(utils.safe_strseq_iter(sharing_users_deny)) ) - self._set_value("sharing_restrict_paths", sharing_restrict_paths) - self._set_value("policies", policies) + + self["delete_protected"] = delete_protected + self["allow_guest_collections"] = allow_guest_collections + self["disable_anonymous_writes"] = disable_anonymous_writes + self["sharing_restrict_paths"] = sharing_restrict_paths + self["policies"] = policies ensure_datatype(self) @@ -357,37 +362,37 @@ def __init__( self, *, # data type - data_type: str | None = None, + data_type: str | MissingType = MISSING, # > common args start < # strs - collection_base_path: str | None = None, - contact_email: str | None = None, - contact_info: str | None = None, - default_directory: str | None = None, - department: str | None = None, - description: str | None = None, - display_name: str | None = None, - identity_id: UUIDLike | None = None, - info_link: str | None = None, - organization: str | None = None, - user_message: str | None = None, - user_message_link: str | None = None, + collection_base_path: str | MissingType = MISSING, + contact_email: str | None | MissingType = MISSING, + contact_info: str | None | MissingType = MISSING, + default_directory: str | MissingType = MISSING, + department: str | None | MissingType = MISSING, + description: str | None | MissingType = MISSING, + display_name: str | MissingType = MISSING, + identity_id: UUIDLike | MissingType = MISSING, + info_link: str | None | MissingType = MISSING, + organization: str | MissingType = MISSING, + user_message: str | None | MissingType = MISSING, + user_message_link: str | None | MissingType = MISSING, # str lists - keywords: t.Iterable[str] | None = None, + keywords: t.Iterable[str] | MissingType = MISSING, # bools - disable_verify: bool | None = None, - enable_https: bool | None = None, - force_encryption: bool | None = None, - force_verify: bool | None = None, - public: bool | None = None, + disable_verify: bool | MissingType = MISSING, + enable_https: bool | MissingType = MISSING, + force_encryption: bool | MissingType = MISSING, + force_verify: bool | MissingType = MISSING, + public: bool | MissingType = MISSING, # > common args end < # > specific args start < - mapped_collection_id: UUIDLike | None = None, - user_credential_id: UUIDLike | None = None, - activity_notification_policy: dict[str, list[str]] | None = None, + mapped_collection_id: UUIDLike | MissingType = MISSING, + user_credential_id: UUIDLike | MissingType = MISSING, + activity_notification_policy: dict[str, list[str]] | MissingType = MISSING, # > specific args end < # additional fields - additional_fields: dict[str, t.Any] | None = None, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> None: super().__init__( # data type @@ -416,12 +421,10 @@ def __init__( # additional fields additional_fields=additional_fields, ) - self._set_optstrs( - mapped_collection_id=mapped_collection_id, - user_credential_id=user_credential_id, - ) - self._set_value("activity_notification_policy", activity_notification_policy) + self["mapped_collection_id"] = mapped_collection_id + self["user_credential_id"] = user_credential_id + self["activity_notification_policy"] = activity_notification_policy ensure_datatype(self) @@ -450,17 +453,25 @@ class POSIXCollectionPolicies(CollectionPolicies): def __init__( self, DATA_TYPE: str = "posix_collection_policies#1.0.0", - sharing_groups_allow: None | str | t.Iterable[str] = None, - sharing_groups_deny: None | str | t.Iterable[str] = None, - additional_fields: dict[str, t.Any] | None = None, + sharing_groups_allow: str | t.Iterable[str] | None | MissingType = MISSING, + sharing_groups_deny: str | t.Iterable[str] | None | MissingType = MISSING, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE) - self._set_optstrlists( - sharing_groups_allow=sharing_groups_allow, - sharing_groups_deny=sharing_groups_deny, + self["DATA_TYPE"] = DATA_TYPE + + self["sharing_groups_allow"] = ( + sharing_groups_allow + if isinstance(sharing_groups_allow, (MissingType, type(None))) + else list(utils.safe_strseq_iter(sharing_groups_allow)) + ) + self["sharing_groups_deny"] = ( + sharing_groups_deny + if isinstance(sharing_groups_deny, (MissingType, type(None))) + else list(utils.safe_strseq_iter(sharing_groups_deny)) ) - if additional_fields is not None: + + if not isinstance(additional_fields, MissingType): self.update(additional_fields) @@ -482,17 +493,24 @@ class POSIXStagingCollectionPolicies(CollectionPolicies): def __init__( self, DATA_TYPE: str = "posix_staging_collection_policies#1.0.0", - sharing_groups_allow: None | str | t.Iterable[str] = None, - sharing_groups_deny: None | str | t.Iterable[str] = None, - additional_fields: dict[str, t.Any] | None = None, + sharing_groups_allow: str | t.Iterable[str] | None | MissingType = MISSING, + sharing_groups_deny: str | t.Iterable[str] | None | MissingType = MISSING, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE) - self._set_optstrlists( - sharing_groups_allow=sharing_groups_allow, - sharing_groups_deny=sharing_groups_deny, + self["DATA_TYPE"] = DATA_TYPE + self["sharing_groups_allow"] = ( + sharing_groups_allow + if isinstance(sharing_groups_allow, (MissingType, type(None))) + else list(utils.safe_strseq_iter(sharing_groups_allow)) + ) + self["sharing_groups_deny"] = ( + sharing_groups_deny + if isinstance(sharing_groups_deny, (MissingType, type(None))) + else list(utils.safe_strseq_iter(sharing_groups_deny)) ) - if additional_fields is not None: + + if not isinstance(additional_fields, MissingType): self.update(additional_fields) @@ -510,10 +528,11 @@ class GoogleCloudStorageCollectionPolicies(CollectionPolicies): def __init__( self, DATA_TYPE: str = "google_cloud_storage_collection_policies#1.0.0", - project: str | None = None, - additional_fields: dict[str, t.Any] | None = None, + project: str | MissingType = MISSING, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE, project=project) - if additional_fields is not None: + self["DATA_TYPE"] = DATA_TYPE + self["project"] = project + if not isinstance(additional_fields, MissingType): self.update(additional_fields) diff --git a/tests/unit/helpers/gcs/test_collections.py b/tests/unit/helpers/gcs/test_collections.py index 16badce00..1a8b41988 100644 --- a/tests/unit/helpers/gcs/test_collections.py +++ b/tests/unit/helpers/gcs/test_collections.py @@ -1,4 +1,5 @@ import inspect +import typing as t import uuid import pytest @@ -12,6 +13,7 @@ POSIXStagingCollectionPolicies, ) from globus_sdk.transport import JSONRequestEncoder +from globus_sdk.utils import MISSING, MissingType, UUIDLike, filter_missing STUB_SG_ID = uuid.uuid1() # storage gateway STUB_MC_ID = uuid.uuid1() # mapped collection @@ -212,18 +214,140 @@ def test_settings_which_are_only_supported_in_guest_collections(fieldname): @pytest.mark.parametrize( "fieldname", ( - "allow_guest_collections", + "disable_verify", + "enable_https", + "force_encryption", + "force_verify", + "public", "delete_protected", + "allow_guest_collections", "disable_anonymous_writes", ), ) @pytest.mark.parametrize("value", (True, False, None)) def test_mapped_collection_opt_bool(fieldname, value): - doc = MappedCollectionDocument( - storage_gateway_id=STUB_SG_ID, collection_base_path="/", **{fieldname: value} + data = {} + if value is not None: + data[fieldname] = value + + doc = filter_missing( + MappedCollectionDocument( + storage_gateway_id=STUB_SG_ID, + collection_base_path="/", + **data, + ) ) if value is not None: assert fieldname in doc assert doc[fieldname] == value else: assert fieldname not in doc + + +common_collection_fields = [ + ("collection_base_path", (str, MissingType)), + ("contact_email", (str, None, MissingType)), + ("contact_info", (str, None, MissingType)), + ("default_directory", (str, MissingType)), + ("department", (str, None, MissingType)), + ("description", (str, None, MissingType)), + ("display_name", (str, MissingType)), + ("identity_id", (UUIDLike, MissingType)), + ("info_link", (str, None, MissingType)), + ("organization", (str, MissingType)), + ("user_message", (str, None, MissingType)), + ("user_message_link", (str, None, MissingType)), + ("keywords", (t.Iterable[str], MissingType)), + ("disable_verify", (bool, MissingType)), + ("enable_https", (bool, MissingType)), + ("force_encryption", (bool, MissingType)), + ("force_verify", (bool, MissingType)), + ("public", (bool, MissingType)), +] + + +mapped_collection_fields = [ + *common_collection_fields, + ("domain_name", (str, MissingType)), + ("guest_auth_policy_id", (UUIDLike, None, MissingType)), + ("disable_anonymous_writes", (bool, MissingType)), + ("policies", (t.Dict[str, t.Any], MissingType)), +] + + +guest_collection_fields = [ + *common_collection_fields, + ("mapped_collection_id", (UUIDLike, MissingType)), + ("user_credential_id", (UUIDLike, MissingType)), + ("activity_notification_policy", (t.Dict[str, t.List[str]], MissingType)), +] + + +def expand_collection_fields(fields): + """ + Expand each collection field into (field, valid_value) + """ + return [ + (param, value) + for param, types in fields + for _type in types + for value in _gen_value(_type) + ] + + +def _gen_value(_type): + """ + Return a list of valid values for type _type. + """ + if _type is MissingType: + return [MISSING] + if _type is None: + return [None] + if _type is str: + return ["STRING"] + if _type is bool: + return [True, False] + if _type is UUIDLike: + return [str(uuid.uuid1()), uuid.uuid1()] + if _type is t.Iterable[str]: + return [[], ["a", "b", "c"]] + if _type is t.Dict[str, t.Any]: + return [{"A": 1}] + if _type is t.Dict[str, t.List[str]]: + return [{"a": ["b", "c"]}] + + raise AssertionError(f"Unexpected Type: {_type}") + + +@pytest.mark.parametrize( + "fieldname,value", + expand_collection_fields(mapped_collection_fields), +) +def test_mapped_collection_fields(fieldname, value): + """ + Verify that each field in the mapped collection document can be set to a valid + value. + """ + data = {} + if value != MISSING: + data[fieldname] = value + + doc = MappedCollectionDocument(**data) + assert doc[fieldname] == value + + +@pytest.mark.parametrize( + "fieldname,value", + expand_collection_fields(guest_collection_fields), +) +def test_guest_collection_fields(fieldname, value): + """ + Verify that each field in the guest collection document can be set to a valid + value. + """ + data = {} + if value != MISSING: + data[fieldname] = value + + doc = GuestCollectionDocument(**data) + assert doc[fieldname] == value From 1522d76dbe8d9f3748280e673487878da0a04b3f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 14 May 2025 13:49:22 -0500 Subject: [PATCH 006/176] Make 'GlobusAPIError.code' default to 'None' (#1190) In the source tree, the default is changed from `"Error"` to `None`. In the tests, various direct tests are updated, as are those which inspect the arguments passed to `super().__init__` and which feed into the repr for an API error. There are no paths in the source tree of the SDK itself which depend upon the old default of `"Error"`. --- ...5052_sirosen_remove_default_code_field.rst | 5 +++ src/globus_sdk/exc/api.py | 4 +-- .../services/auth/test_auth_client_flow.py | 4 +-- tests/unit/errors/test_auth_errors.py | 4 +-- .../unit/errors/test_common_functionality.py | 32 +++++++------------ tests/unit/errors/test_timers_errors.py | 2 +- 6 files changed, 24 insertions(+), 27 deletions(-) create mode 100644 changelog.d/20250513_185052_sirosen_remove_default_code_field.rst diff --git a/changelog.d/20250513_185052_sirosen_remove_default_code_field.rst b/changelog.d/20250513_185052_sirosen_remove_default_code_field.rst new file mode 100644 index 000000000..468c148bd --- /dev/null +++ b/changelog.d/20250513_185052_sirosen_remove_default_code_field.rst @@ -0,0 +1,5 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- The default for ``GlobusAPIError.code`` is now ``None``, when no ``code`` is + supplied in the error body. It previously was ``"Error"``. (:pr:`NUMBER`) diff --git a/src/globus_sdk/exc/api.py b/src/globus_sdk/exc/api.py index 6eba66a76..9839ff608 100644 --- a/src/globus_sdk/exc/api.py +++ b/src/globus_sdk/exc/api.py @@ -29,7 +29,7 @@ class GlobusAPIError(GlobusError): Wraps errors returned by a REST API. :ivar int http_status: HTTP status code - :ivar str code: Error code from the API or "Error" for unclassified errors + :ivar str code: Error code from the API or ``None`` for unclassified errors :ivar str request_id: The 'request_id' included in the error data, if any. :ivar list[str] messages: A list of error messages, extracted from the response data. If the data cannot be parsed or does not contain any clear message fields, @@ -46,7 +46,7 @@ def __init__(self, r: requests.Response, *args: t.Any, **kwargs: t.Any) -> None: self.http_status = r.status_code # defaults, may be rewritten during parsing - self.code: str | None = "Error" + self.code: str | None = None self.request_id: str | None = None self.messages: list[str] = [] self.errors: list[ErrorSubdocument] = [] diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index 367ed58b7..5e7943140 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -299,7 +299,7 @@ def test_oauth2_exchange_code_for_tokens_native(native_client): with pytest.raises(globus_sdk.AuthAPIError) as excinfo: native_client.oauth2_exchange_code_for_tokens("invalid_code") assert excinfo.value.http_status == 401 - assert excinfo.value.code == "Error" + assert excinfo.value.code is None def test_oauth2_exchange_code_for_tokens_confidential(confidential_client): @@ -319,4 +319,4 @@ def test_oauth2_exchange_code_for_tokens_confidential(confidential_client): with pytest.raises(globus_sdk.AuthAPIError) as excinfo: confidential_client.oauth2_exchange_code_for_tokens("invalid_code") assert excinfo.value.http_status == 401 - assert excinfo.value.code == "Error" + assert excinfo.value.code is None diff --git a/tests/unit/errors/test_auth_errors.py b/tests/unit/errors/test_auth_errors.py index 6178e60f9..0103a2feb 100644 --- a/tests/unit/errors/test_auth_errors.py +++ b/tests/unit/errors/test_auth_errors.py @@ -17,7 +17,7 @@ def test_auth_error_get_args_simple(): req.url, None, 404, - "Error", + None, "simple auth error message", ] @@ -38,7 +38,7 @@ def test_nested_auth_error_message_and_code(): ) assert err.message == "nested auth error message; some secondary error" - assert err.code == "Error" + assert err.code is None @pytest.mark.parametrize( diff --git a/tests/unit/errors/test_common_functionality.py b/tests/unit/errors/test_common_functionality.py index 72a934abc..775418dd6 100644 --- a/tests/unit/errors/test_common_functionality.py +++ b/tests/unit/errors/test_common_functionality.py @@ -86,13 +86,13 @@ def test_imperative_message_setting_warns(): @pytest.mark.parametrize( "body, response_headers, http_status, expect_code, expect_message", ( - ("text_data", {}, 401, "Error", "Unauthorized"), # text + ("text_data", {}, 401, None, "Unauthorized"), # text # JSON with unrecognized contents ( {"foo": "bar"}, {"Content-Type": "application/json"}, 403, - "Error", + None, "Forbidden", ), # JSON with well-known contents @@ -108,7 +108,7 @@ def test_imperative_message_setting_warns(): "[]", {"Content-Type": "application/json"}, 403, - "Error", + None, "Forbidden", ), # invalid JSON @@ -116,7 +116,7 @@ def test_imperative_message_setting_warns(): "{", {"Content-Type": "application/json"}, 400, - "Error", + None, "Bad Request", ), ), @@ -544,7 +544,8 @@ def test_error_repr_has_expected_info( if error_code is not None: assert error_code in stringified else: - assert "'Error'" in stringified + # several things could be 'None', but at least one of them is 'code' + assert "None" in stringified if error_message is None: assert "otherdata" in stringified else: @@ -584,16 +585,10 @@ def test_loads_jsonapi_error_subdocuments(content_type): ) # code is not taken from any of the subdocuments (inherently too ambiguous) - # behavior will depend on which parsing path was taken - if content_type.endswith("vnd.api+json"): - # code becomes None because we saw "true" JSON:API and can opt-in to - # better behavior - assert err.code is None - else: - # code remains 'Error' for backwards compatibility in the non-JSON:API case - assert err.code == "Error" + # this holds regardless of which parsing path was taken + assert err.code is None - # but messages can be extracted, and they prefer detail to title + # messages can be extracted, and they prefer detail to title assert err.messages == [ "password was only 3 chars long, must be at least 8", "password must have non-alphanumeric characters", @@ -649,22 +644,19 @@ def test_loads_jsonapi_error_messages_from_various_fields(content_type): body=body, http_status=422, response_headers={"Content-Type": content_type} ) + # no code was found + assert err.code is None + # messages are extracted, and they use whichever field is appropriate for # each sub-error # note that 'message' will *not* be extracted if the Content-Type indicated JSON:API # because JSON:API does not define such a field if content_type.endswith("vnd.api+json"): - # code becomes None because we saw "true" JSON:API and can opt-in to - # better behavior - assert err.code is None assert err.messages == [ "Must contain capital letter", "password must have non-alphanumeric characters", ] else: - # code remains 'Error' for backwards compatibility in the non-JSON:API case - assert err.code == "Error" - assert err.messages == [ "invalid password value", "Must contain capital letter", diff --git a/tests/unit/errors/test_timers_errors.py b/tests/unit/errors/test_timers_errors.py index 8cf05246b..26d1b29fb 100644 --- a/tests/unit/errors/test_timers_errors.py +++ b/tests/unit/errors/test_timers_errors.py @@ -39,5 +39,5 @@ def test_timer_error_load_nested(): def test_timer_error_load_unrecognized_format(): err = construct_error(error_class=TimersAPIError, body={}, http_status=400) - assert err.code == "Error" + assert err.code is None assert err.message is None From 613aed94bb75b9e8a2deb338e39b04707a3bba53 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 15 May 2025 18:06:19 -0400 Subject: [PATCH 007/176] Simplify custom parsing for TimersAPIError (#1191) In particular, remove the special-case `code` default. Also, make use of subdocument message parsing to capture the `msg` field of sub-errors. --- ...011903_sirosen_simplify_timersapierror.rst | 6 +++ src/globus_sdk/services/timers/errors.py | 37 ++++++++----------- tests/functional/services/timers/test_jobs.py | 2 +- tests/unit/errors/test_timers_errors.py | 2 +- 4 files changed, 24 insertions(+), 23 deletions(-) create mode 100644 changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst diff --git a/changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst b/changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst new file mode 100644 index 000000000..54a33032f --- /dev/null +++ b/changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst @@ -0,0 +1,6 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- ``TimersAPIError`` no longer sets ``code="ValidationError"`` when an error + with no code which appears to be validation related is parsed. Like other + error classes, the default when no ``code`` is set is ``None``. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/timers/errors.py b/src/globus_sdk/services/timers/errors.py index f3f6f4008..5654febdb 100644 --- a/src/globus_sdk/services/timers/errors.py +++ b/src/globus_sdk/services/timers/errors.py @@ -10,9 +10,8 @@ class TimersAPIError(GlobusAPIError): """ Error class to represent error responses from Timers. - Has no particular additions to the base ``GlobusAPIError``, but implements a - different method for parsing error responses from Timers due to the differences - between various error formats used. + Implements a dedicated method for parsing error responses from Timers due + to the differences between various error formats used. """ def _parse_undefined_error_format(self) -> bool: @@ -48,42 +47,38 @@ def _parse_undefined_error_format(self) -> bool: # but before that fallback, try the two relevant branches # if 'error' is present, use it to populate the errors array - # extract 'code' from it - # and extract 'messages' from it + # extract 'code' and 'messages' from it if isinstance(self._dict_data.get("error"), dict): self.errors = [ErrorSubdocument(self._dict_data["error"])] self.code = self._extract_code_from_error_array(self.errors) self.messages = self._extract_messages_from_error_array(self.errors) return True elif _guards.is_list_of(self._dict_data.get("detail"), dict): - # FIXME: - # the 'code' is currently being set explicitly by the - # SDK in this case even though none was provided by - # the service - # in a future version of the SDK, the code should be `None` - self.code = "Validation Error" - # collect the errors array from details - self.errors = [ErrorSubdocument(d) for d in self._dict_data["detail"]] + self.errors = [ + ErrorSubdocument(d, message_fields=("msg",)) + for d in self._dict_data["detail"] + ] + # extract a 'code' if there is one + self.code = self._extract_code_from_error_array(self.errors) - # drop error objects which don't have the relevant fields - # and then build custom 'messages' for Globus Timers errors - details = list(_details_from_errors(self.errors)) + # build custom 'messages' for this case self.messages = [ - f"{e['msg']}: {'.'.join(k for k in e['loc'])}" for e in details + f"{message}: {loc}" + for (message, loc) in _parse_detail_docs(self.errors) ] return True else: return super()._parse_undefined_error_format() -def _details_from_errors( +def _parse_detail_docs( errors: list[ErrorSubdocument], -) -> t.Iterator[dict[str, t.Any]]: +) -> t.Iterator[tuple[str, str]]: for d in errors: - if not isinstance(d.get("msg"), str): + if d.message is None: continue loc_list = d.get("loc") if not _guards.is_list_of(loc_list, str): continue - yield d.raw + yield (d.message, ".".join(loc_list)) diff --git a/tests/functional/services/timers/test_jobs.py b/tests/functional/services/timers/test_jobs.py index 1a3fcc28a..ed84b4515 100644 --- a/tests/functional/services/timers/test_jobs.py +++ b/tests/functional/services/timers/test_jobs.py @@ -80,7 +80,7 @@ def test_create_job_validation_error(client): err = excinfo.value assert err.http_status == 422 - assert err.code == "Validation Error" + assert err.code is None assert err.messages == meta["expect_messages"] diff --git a/tests/unit/errors/test_timers_errors.py b/tests/unit/errors/test_timers_errors.py index 26d1b29fb..5125d495a 100644 --- a/tests/unit/errors/test_timers_errors.py +++ b/tests/unit/errors/test_timers_errors.py @@ -33,7 +33,7 @@ def test_timer_error_load_nested(): http_status=422, ) - assert err.code == "Validation Error" + assert err.code is None assert err.message == "field required: body.start; field required: body.end" From d4229f7f761880f41c1decbe376c86975cb4131b Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Tue, 20 May 2025 10:43:31 -0500 Subject: [PATCH 008/176] Bump version and changelog for release --- ...2_143605_sirosen_remove_default_scopes.rst | 8 --- ...sc_26346_remove_base_path_from_clients.rst | 5 -- changelog.d/20250513_182417_jasonalt.rst | 53 ------------------- ...5052_sirosen_remove_default_code_field.rst | 5 -- ...011903_sirosen_simplify_timersapierror.rst | 6 --- changelog.rst | 28 ++++++++++ 6 files changed, 28 insertions(+), 77 deletions(-) delete mode 100644 changelog.d/20250512_143605_sirosen_remove_default_scopes.rst delete mode 100644 changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst delete mode 100644 changelog.d/20250513_182417_jasonalt.rst delete mode 100644 changelog.d/20250513_185052_sirosen_remove_default_code_field.rst delete mode 100644 changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst diff --git a/changelog.d/20250512_143605_sirosen_remove_default_scopes.rst b/changelog.d/20250512_143605_sirosen_remove_default_scopes.rst deleted file mode 100644 index be02c475a..000000000 --- a/changelog.d/20250512_143605_sirosen_remove_default_scopes.rst +++ /dev/null @@ -1,8 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- The SDK no longer sets default scopes for direct use of client - credentials and auth client login flow methods. Users should either use - ``GlobusApp`` objects, which can specify scopes based on the clients in use, - or else pass a list of scopes explicitly to - ``oauth2_client_credentials_tokens`` or ``oauth2_start_flow``. (:pr:`1186`) diff --git a/changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst b/changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst deleted file mode 100644 index 80ec1266e..000000000 --- a/changelog.d/20250512_144528_max.tuecke_sc_26346_remove_base_path_from_clients.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -~~~~~~~ - -- SDK client classes no longer define nor prepend a ``base_path`` attribute which they prefix to paths. - Make sure to use the full path now when using client methods. (:pr:`1185`) \ No newline at end of file diff --git a/changelog.d/20250513_182417_jasonalt.rst b/changelog.d/20250513_182417_jasonalt.rst deleted file mode 100644 index 59b66bf72..000000000 --- a/changelog.d/20250513_182417_jasonalt.rst +++ /dev/null @@ -1,53 +0,0 @@ -.. -.. A new scriv changelog fragment -.. -.. Uncomment the header that is right (remove the leading dots). -.. -.. Leave the "(:pr:`...`)" text in your change description. -.. GitHub Actions will automatically replace it when the PR is merged. -.. -.. Python Support -.. ~~~~~~~~~~~~~~ -.. -.. - A bullet item for the Python Support category. (:pr:`NUMBER`) -.. -.. Added -.. ~~~~~ -.. -.. - A bullet item for the Added category. (:pr:`NUMBER`) -.. -.. Removed -.. ~~~~~~~ -.. -.. - A bullet item for the Removed category. (:pr:`NUMBER`) -.. -Changed -~~~~~~~ - -- Updated MappedCollectionDoc and GuestCollectionDoc with MissingType. (:pr:`NUMBER`) - -.. Deprecated -.. ~~~~~~~~~~ -.. -.. - A bullet item for the Deprecated category. (:pr:`NUMBER`) -.. -.. Fixed -.. ~~~~~ -.. -.. - A bullet item for the Fixed category. (:pr:`NUMBER`) -.. -.. Documentation -.. ~~~~~~~~~~~~~ -.. -.. - A bullet item for the Documentation category. (:pr:`NUMBER`) -.. -.. Security -.. ~~~~~~~~ -.. -.. - A bullet item for the Security category. (:pr:`NUMBER`) -.. -.. Development -.. ~~~~~~~~~~~ -.. -.. - A bullet item for the Development category. (:pr:`NUMBER`) -.. diff --git a/changelog.d/20250513_185052_sirosen_remove_default_code_field.rst b/changelog.d/20250513_185052_sirosen_remove_default_code_field.rst deleted file mode 100644 index 468c148bd..000000000 --- a/changelog.d/20250513_185052_sirosen_remove_default_code_field.rst +++ /dev/null @@ -1,5 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- The default for ``GlobusAPIError.code`` is now ``None``, when no ``code`` is - supplied in the error body. It previously was ``"Error"``. (:pr:`NUMBER`) diff --git a/changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst b/changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst deleted file mode 100644 index 54a33032f..000000000 --- a/changelog.d/20250514_011903_sirosen_simplify_timersapierror.rst +++ /dev/null @@ -1,6 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- ``TimersAPIError`` no longer sets ``code="ValidationError"`` when an error - with no code which appears to be validation related is parsed. Like other - error classes, the default when no ``code`` is set is ``None``. (:pr:`NUMBER`) diff --git a/changelog.rst b/changelog.rst index 773b2cff9..8b298ca40 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,34 @@ to a major new version of the SDK. .. scriv-insert-here +.. _changelog-4.0.0a1: + +v4.0.0a1 (2025-05-20) +--------------------- + +Breaking Changes +~~~~~~~~~~~~~~~~ + +- The SDK no longer sets default scopes for direct use + of client credentials and auth client login flow methods. + Users should either use ``GlobusApp`` objects, + which can specify scopes based on the clients in use, + or else pass a list of scopes explicitly to + ``oauth2_client_credentials_tokens`` or ``oauth2_start_flow``. (:pr:`1186`) + +- The default ``GlobusAPIError.code`` value is now ``None`` + when ``code`` is not supplied in the error body. + Previously, the default was ``"Error"``. (:pr:`1190`) + +- The default ``TimersAPIError.code`` value is now ``None`` + when an error which appears to be validation-related has no ``code``. + Previously, the default was ``"ValidationError"``. (:pr:`1191`) + +- SDK client classes no longer define nor prepend a ``base_path`` attribute to paths. + Make sure to use the full path now when using client methods. (:pr:`1185`) + +- Updated MappedCollectionDoc and GuestCollectionDoc with MissingType. (:pr:`1189`) + .. _changelog-3.56.1: v3.56.1 (2025-05-20) From 6a8e22e10efc602bb384d1d4d6044b6cd1850f8c Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Tue, 20 May 2025 12:10:54 -0500 Subject: [PATCH 009/176] Remove the `globus_sdk.version` module --- Makefile | 2 +- RELEASING.md | 4 ++-- ...6_kurtmckee_rm_executable_version_code.rst | 12 ++++++++++ changelog.d/check-version-is-new.py | 22 +++++++++---------- docs/upgrading.rst | 6 ++--- pyproject.toml | 7 ++---- scripts/rtd-pre-sphinx-build.sh | 2 +- src/globus_sdk/__init__.py | 4 +++- src/globus_sdk/__init__.pyi | 3 ++- .../tokenstorage/v1/file_adapters.py | 2 +- .../tokenstorage/v1/sqlite_adapter.py | 2 +- src/globus_sdk/tokenstorage/v2/json.py | 2 +- src/globus_sdk/tokenstorage/v2/sqlite.py | 3 +-- src/globus_sdk/transport/_clientinfo.py | 3 +-- src/globus_sdk/transport/requests.py | 3 +-- src/globus_sdk/version.py | 3 --- .../tokenstorage/v1/test_simplejson_file.py | 2 +- .../tokenstorage/v2/test_json_tokenstorage.py | 2 +- .../test_modules_do_not_require_requests.py | 1 - .../v1/test_simplejson_adapter.py | 2 +- 20 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst delete mode 100644 src/globus_sdk/version.py diff --git a/Makefile b/Makefile index f88400f69..c3dbbd7d1 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SDK_VERSION=$(shell grep '^__version__' src/globus_sdk/version.py | cut -d '"' -f2) +SDK_VERSION=$(shell grep '^version' pyproject.toml | head -n 1 | cut -d '"' -f2) # these are just tox invocations wrapped nicely for convenience .PHONY: lint test docs all-checks diff --git a/RELEASING.md b/RELEASING.md index 6e0a45a0c..80cab786a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -18,7 +18,7 @@ - Decide on the new version number and create a branch; `git checkout -b release-$SDK_VERSION` -- Update the version in `src/globus_sdk/version.py` +- Update the version in `pyproject.toml` - Update metadata and changelog, then verify changes in `changelog.rst` @@ -28,7 +28,7 @@ $EDITOR changelog.rst ``` - Add changed files; - `git add changelog.d/ changelog.rst src/globus_sdk/version.py` + `git add changelog.d/ changelog.rst pyproject.toml` - Commit; `git commit -m 'Bump version and changelog for release'` diff --git a/changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst b/changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst new file mode 100644 index 000000000..d10b66bda --- /dev/null +++ b/changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst @@ -0,0 +1,12 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- The SDK version is no longer available in ``globus_sdk.version.__version__``. (:pr:`NUMBER`) + + Packages that want to query the SDK version must use ``importlib.metadata``: + + .. code-block:: python + + import importlib.metadata + + GLOBUS_SDK_VERSION = importlib.metadata.distribution("globus_sdk").version diff --git a/changelog.d/check-version-is-new.py b/changelog.d/check-version-is-new.py index f514b9523..5e3777ba5 100755 --- a/changelog.d/check-version-is-new.py +++ b/changelog.d/check-version-is-new.py @@ -6,24 +6,22 @@ import re import sys +if sys.version_info >= (3, 11): + import tomllib +else: + # Older Python versions + import tomli as tomllib + PATTERN_FORMAT = "^v{version}\\s+\\({date}\\)$" CHANGELOG_D = os.path.dirname(__file__) REPO_ROOT = os.path.dirname(CHANGELOG_D) def parse_version(): - # single source of truth for package version - version_string = "" - version_pattern = re.compile(r'__version__ = "([^"]*)"') - with open(os.path.join(REPO_ROOT, "src", "globus_sdk", "version.py")) as f: - for line in f: - match = version_pattern.match(line) - if match: - version_string = match.group(1) - break - if not version_string: - raise RuntimeError("Failed to parse version information") - return version_string + with open(os.path.join(REPO_ROOT, "pyproject.toml"), "rb") as f: + pyproject = tomllib.load(f) + + return pyproject["project"]["version"] def get_header_re(version): diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 3d2b59860..7b1784910 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -25,10 +25,10 @@ the globus-sdk at the same time, consider adding this snippet: .. code-block:: python - import globus_sdk + import importlib.metadata - GLOBUS_SDK_VERSION = tuple(globus_sdk.__version__.split(".")) - GLOBUS_SDK_MAJOR_VERSION = int(GLOBUS_SDK_VERSION[0]) + GLOBUS_SDK_VERSION = importlib.metadata.distribution("globus_sdk").version + GLOBUS_SDK_MAJOR_VERSION = int(GLOBUS_SDK_VERSION.split(".")[0]) This will parse the Globus SDK version information into a tuple and grab the first element (the major version number) as an integer. diff --git a/pyproject.toml b/pyproject.toml index 40281e707..6ab7cd8a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "globus-sdk" +version = "4.0.0a1" authors = [ { name = "Globus Team", email = "support@globus.org" }, ] @@ -36,7 +37,6 @@ dependencies = [ # python versions older than 3.9 don't have importlib.resources 'importlib_resources>=5.12.0; python_version<"3.9"', ] -dynamic = ["version"] [project.readme] file = "README.rst" @@ -100,9 +100,6 @@ globus_sdk = [ ] "globus_sdk.login_flows.local_server_login_flow_manager.html_files" = ["*.html"] -[tool.setuptools.dynamic.version] -attr = "globus_sdk.__version__" - # non-packaging tool configs follow [tool.pytest.ini_options] @@ -149,7 +146,7 @@ exclude_lines =[ ] [tool.scriv] -version = "literal: src/globus_sdk/version.py: __version__" +version = "literal: pyproject.toml: project.version" format = "rst" output_file = "changelog.rst" entry_title_template = 'v{{ version }} ({{ date.strftime("%Y-%m-%d") }})' diff --git a/scripts/rtd-pre-sphinx-build.sh b/scripts/rtd-pre-sphinx-build.sh index ae4c98515..e4d625245 100755 --- a/scripts/rtd-pre-sphinx-build.sh +++ b/scripts/rtd-pre-sphinx-build.sh @@ -1,6 +1,6 @@ #!/bin/bash -VERSION=$(grep '^__version__' src/globus_sdk/version.py | cut -d '"' -f2) +VERSION=$(grep '^version' pyproject.toml | head -n 1 | cut -d '"' -f2) case "$READTHEDOCS_VERSION_TYPE" in external) diff --git a/src/globus_sdk/__init__.py b/src/globus_sdk/__init__.py index d95f12e93..c408ca509 100644 --- a/src/globus_sdk/__init__.py +++ b/src/globus_sdk/__init__.py @@ -1,3 +1,4 @@ +import importlib.metadata import logging import sys @@ -6,7 +7,8 @@ default_getattr_implementation, load_all_tuple, ) -from .version import __version__ # noqa: F401 + +__version__ = importlib.metadata.distribution("globus_sdk").version def _force_eager_imports() -> None: diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index d72d72209..61fd2ca7a 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -127,7 +127,8 @@ from .services.transfer import ( TransferData, ) from .utils import MISSING, MissingType -from .version import __version__ + +__version__ = "x.y.z" def _force_eager_imports() -> None: ... diff --git a/src/globus_sdk/tokenstorage/v1/file_adapters.py b/src/globus_sdk/tokenstorage/v1/file_adapters.py index 909817143..f5655a2f6 100644 --- a/src/globus_sdk/tokenstorage/v1/file_adapters.py +++ b/src/globus_sdk/tokenstorage/v1/file_adapters.py @@ -5,7 +5,7 @@ import typing as t import globus_sdk -from globus_sdk.version import __version__ +from globus_sdk import __version__ from .base import FileAdapter diff --git a/src/globus_sdk/tokenstorage/v1/sqlite_adapter.py b/src/globus_sdk/tokenstorage/v1/sqlite_adapter.py index 4e441c2ce..28f08bf6e 100644 --- a/src/globus_sdk/tokenstorage/v1/sqlite_adapter.py +++ b/src/globus_sdk/tokenstorage/v1/sqlite_adapter.py @@ -6,7 +6,7 @@ import typing as t import globus_sdk -from globus_sdk.version import __version__ +from globus_sdk import __version__ from .base import FileAdapter diff --git a/src/globus_sdk/tokenstorage/v2/json.py b/src/globus_sdk/tokenstorage/v2/json.py index 122d8fb8c..0d559ab1e 100644 --- a/src/globus_sdk/tokenstorage/v2/json.py +++ b/src/globus_sdk/tokenstorage/v2/json.py @@ -3,7 +3,7 @@ import json import typing as t -from globus_sdk.version import __version__ +from globus_sdk import __version__ from .base import FileTokenStorage from .token_data import TokenStorageData diff --git a/src/globus_sdk/tokenstorage/v2/sqlite.py b/src/globus_sdk/tokenstorage/v2/sqlite.py index 2f4b7c8db..cbd0fb7b0 100644 --- a/src/globus_sdk/tokenstorage/v2/sqlite.py +++ b/src/globus_sdk/tokenstorage/v2/sqlite.py @@ -6,8 +6,7 @@ import textwrap import typing as t -from globus_sdk import exc -from globus_sdk.version import __version__ +from globus_sdk import __version__, exc from .base import FileTokenStorage from .token_data import TokenStorageData diff --git a/src/globus_sdk/transport/_clientinfo.py b/src/globus_sdk/transport/_clientinfo.py index c7d9078f2..8a0383862 100644 --- a/src/globus_sdk/transport/_clientinfo.py +++ b/src/globus_sdk/transport/_clientinfo.py @@ -10,8 +10,7 @@ import typing as t -from globus_sdk import exc -from globus_sdk.version import __version__ +from globus_sdk import __version__, exc _RESERVED_CHARS = ";,=" diff --git a/src/globus_sdk/transport/requests.py b/src/globus_sdk/transport/requests.py index 3712ae6cd..cfac09b55 100644 --- a/src/globus_sdk/transport/requests.py +++ b/src/globus_sdk/transport/requests.py @@ -9,14 +9,13 @@ import requests -from globus_sdk import config, exc, utils +from globus_sdk import __version__, config, exc, utils from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.transport.encoders import ( FormRequestEncoder, JSONRequestEncoder, RequestEncoder, ) -from globus_sdk.version import __version__ from ._clientinfo import GlobusClientInfo from .retry import ( diff --git a/src/globus_sdk/version.py b/src/globus_sdk/version.py deleted file mode 100644 index 47cf67f48..000000000 --- a/src/globus_sdk/version.py +++ /dev/null @@ -1,3 +0,0 @@ -# single source of truth for package version, -# see https://packaging.python.org/en/latest/single_source_version/ -__version__ = "4.0.0a1" diff --git a/tests/functional/tokenstorage/v1/test_simplejson_file.py b/tests/functional/tokenstorage/v1/test_simplejson_file.py index 7385a9d70..ed6153ac4 100644 --- a/tests/functional/tokenstorage/v1/test_simplejson_file.py +++ b/tests/functional/tokenstorage/v1/test_simplejson_file.py @@ -3,8 +3,8 @@ import pytest +from globus_sdk import __version__ from globus_sdk.tokenstorage import SimpleJSONFileAdapter -from globus_sdk.version import __version__ IS_WINDOWS = os.name == "nt" diff --git a/tests/functional/tokenstorage/v2/test_json_tokenstorage.py b/tests/functional/tokenstorage/v2/test_json_tokenstorage.py index 606fab039..04221130e 100644 --- a/tests/functional/tokenstorage/v2/test_json_tokenstorage.py +++ b/tests/functional/tokenstorage/v2/test_json_tokenstorage.py @@ -3,8 +3,8 @@ import pytest +from globus_sdk import __version__ from globus_sdk.tokenstorage import JSONTokenStorage, SimpleJSONFileAdapter -from globus_sdk.version import __version__ IS_WINDOWS = os.name == "nt" diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index d28731557..681cf6339 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -39,7 +39,6 @@ "_serializable", "_types", "utils", - "version", ), ) def test_module_does_not_require_requests(module_name): diff --git a/tests/unit/tokenstorage/v1/test_simplejson_adapter.py b/tests/unit/tokenstorage/v1/test_simplejson_adapter.py index 321b3b155..ecf3c13a7 100644 --- a/tests/unit/tokenstorage/v1/test_simplejson_adapter.py +++ b/tests/unit/tokenstorage/v1/test_simplejson_adapter.py @@ -2,8 +2,8 @@ import pytest +from globus_sdk import __version__ as sdkversion from globus_sdk.tokenstorage import SimpleJSONFileAdapter -from globus_sdk.version import __version__ as sdkversion def test_simplejson_reading_bad_data(tmp_path): From 89df1de5c3a41517780fe6c0af9606fbaedf9c97 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 28 May 2025 17:54:57 -0500 Subject: [PATCH 010/176] Remove the 'MutableScope' type (#1198) * Remove the 'MutableScope' type This is deprecated in 3.x in favor of the 'Scope' type. As part of our 4.0 release, we will be removing the 'MutableScope' type. This removes the type itself, and then handles all of the cascading fixes this necessitates. No update to the upgrading doc is included yet, as we have additional plans to make changes to the scope interfaces. * Minor fix to a mypy-test `Scope.scopes2str` does not exist -- this test was created by a `MutableScope` replacement but the `Scope` and `MutableScope` interfaces differ here. Remove the bad test case. --- ...28_150526_sirosen_remove_mutable_scope.rst | 7 ++ .../scopes_and_consents/index.rst | 1 - .../scopes_and_consents/mutable_scopes.rst | 95 -------------- docs/examples/guest_collection_creation.rst | 2 +- src/globus_sdk/_types.py | 3 +- src/globus_sdk/scopes/__init__.py | 2 - src/globus_sdk/scopes/_normalize.py | 11 +- src/globus_sdk/scopes/builder.py | 30 ----- src/globus_sdk/scopes/scope_definition.py | 119 ------------------ .../test_oauth2_client_credentials_tokens.py | 6 +- .../scope_collection_type.py | 54 ++++---- .../test_client_credentials_authorizer.py | 8 +- .../unit/helpers/test_auth_scope_stringify.py | 8 +- tests/unit/scopes/test_mutable_scope.py | 48 ------- tests/unit/scopes/test_scope_builder.py | 11 -- tests/unit/scopes/test_scope_normalization.py | 15 ++- 16 files changed, 54 insertions(+), 366 deletions(-) create mode 100644 changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst delete mode 100644 docs/authorization/scopes_and_consents/mutable_scopes.rst delete mode 100644 src/globus_sdk/scopes/scope_definition.py delete mode 100644 tests/unit/scopes/test_mutable_scope.py diff --git a/changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst b/changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst new file mode 100644 index 000000000..23e8e1ab5 --- /dev/null +++ b/changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst @@ -0,0 +1,7 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- The legacy ``MutableScope`` type has been removed. (:pr:`1198`) + + - The ``make_mutable`` method on ``ScopeBuilder`` objects has also been + removed as a consequence of this change. diff --git a/docs/authorization/scopes_and_consents/index.rst b/docs/authorization/scopes_and_consents/index.rst index 0f16517d8..03998e1a7 100644 --- a/docs/authorization/scopes_and_consents/index.rst +++ b/docs/authorization/scopes_and_consents/index.rst @@ -30,5 +30,4 @@ which make learning about and manipulating these data easier. :maxdepth: 1 scopes - mutable_scopes consents diff --git a/docs/authorization/scopes_and_consents/mutable_scopes.rst b/docs/authorization/scopes_and_consents/mutable_scopes.rst deleted file mode 100644 index 17dd8dff3..000000000 --- a/docs/authorization/scopes_and_consents/mutable_scopes.rst +++ /dev/null @@ -1,95 +0,0 @@ -.. _mutable_scopes: - -.. currentmodule:: globus_sdk.scopes - -MutableScopes -============= - -.. warning:: - - The ``MutableScope`` class and its interfaces are considered a legacy - feature. They will be deprecated and removed in a future SDK release. - - Users should prefer to use ``globus_sdk.Scope`` instead. - ``globus_sdk.Scope``, documented in :ref:`the scopes documentation `, - provides strictly more features and has a superior interface. - -In order to support optional and dependent scopes, a type is -provided by ``globus_sdk.scopes``: the ``MutableScope`` class. - -``MutableScope`` can be constructed directly or via a ``ScopeBuilder``'s -``make_mutable`` method, given a scope's short name. - -For example, one can create a ``MutableScope`` from the Groups "all" scope -as follows: - -.. code-block:: python - - from globus_sdk.scopes import GroupsScopes - - scope = GroupsScopes.make_mutable("all") - -``MutableScope`` objects primarily provide two main pieces of functionality: -dynamically building a scope tree and serializing to a string. - -Dynamic Scope Construction -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``MutableScope`` objects provide a tree-like interface for constructing scopes -and their dependencies. - -For example, the transfer scope dependent upon a collection scope may be -constructed by means of ``MutableScope`` methods and the ``make_mutable`` method -of scope builders thusly: - -.. code-block:: python - - from globus_sdk.scopes import GCSCollectionScopeBuilder, TransferScopes - - MAPPED_COLLECTION_ID = "...ID HERE..." - - # create the scopes with make_mutable - transfer_scope = TransferScopes.make_mutable("all") - data_access_scope = GCSCollectionScopeBuilder(MAPPED_COLLECTION_ID).make_mutable( - "data_access", optional=True - ) - # add data_access as a dependency - transfer_scope.add_dependency(data_access_scope) - -``MutableScope``\s can be used in most of the same locations where scope -strings can be used, but you can also call ``str()`` on them to get a -stringified representation. - -Serializing Scopes -~~~~~~~~~~~~~~~~~~ - -Whenever scopes are being sent to Globus services, they need to be encoded as -strings. All mutable scope objects support this by means of their defined -``serialize`` method. Note that ``__str__`` for a ``MutableScope`` is just an -alias for ``serialize``. For example, the following is valid usage to demonstrate -``str()``, ``repr()``, and ``serialize()``: - -.. code-block:: pycon - - >>> from globus_sdk.scopes import MutableScope - >>> foo = MutableScope("foo") - >>> bar = MutableScope("bar") - >>> bar.add_dependency("baz") - >>> foo.add_dependency(bar) - >>> print(str(foo)) - foo[bar[baz]] - >>> print(bar.serialize()) - bar[baz] - >>> alpha = MutableScope("alpha") - >>> alpha.add_dependency(MutableScope("beta", optional=True)) - >>> print(str(alpha)) - alpha[*beta] - >>> print(repr(alpha)) - MutableScope("alpha", dependencies=[MutableScope("beta", optional=True)]) - -MutableScope Reference -~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: MutableScope - :members: - :show-inheritance: diff --git a/docs/examples/guest_collection_creation.rst b/docs/examples/guest_collection_creation.rst index 468f032d6..51071be68 100644 --- a/docs/examples/guest_collection_creation.rst +++ b/docs/examples/guest_collection_creation.rst @@ -39,7 +39,7 @@ policy documents passed to create the user credential. # The scope the client will need, note that primary scope is for the endpoint, # but it has a dependency on the mapped collection's data_access scope - scope = scopes.GCSEndpointScopeBuilder(endpoint_id).make_mutable("manage_collections") + scope = scopes.Scope(scopes.GCSEndpointScopeBuilder(endpoint_id).manage_collections) scope.add_dependency(scopes.GCSCollectionScopeBuilder(mapped_collection_id).data_access) # Build a GCSClient to act as the client by using a ClientCredentialsAuthorizor diff --git a/src/globus_sdk/_types.py b/src/globus_sdk/_types.py index 525a928fe..331e591c5 100644 --- a/src/globus_sdk/_types.py +++ b/src/globus_sdk/_types.py @@ -5,7 +5,7 @@ import uuid if t.TYPE_CHECKING: - from globus_sdk.scopes import MutableScope, Scope + from globus_sdk.scopes import Scope # these types are aliases meant for internal use @@ -15,7 +15,6 @@ ScopeCollectionType = t.Union[ str, - "MutableScope", "Scope", t.Iterable["ScopeCollectionType"], ] diff --git a/src/globus_sdk/scopes/__init__.py b/src/globus_sdk/scopes/__init__.py index bfd6b04a2..78f9368b2 100644 --- a/src/globus_sdk/scopes/__init__.py +++ b/src/globus_sdk/scopes/__init__.py @@ -18,11 +18,9 @@ ) from .errors import ScopeCycleError, ScopeParseError from .representation import Scope -from .scope_definition import MutableScope __all__ = ( "ScopeBuilder", - "MutableScope", "Scope", "ScopeParseError", "ScopeCycleError", diff --git a/src/globus_sdk/scopes/_normalize.py b/src/globus_sdk/scopes/_normalize.py index 5ee32c475..f84c968e4 100644 --- a/src/globus_sdk/scopes/_normalize.py +++ b/src/globus_sdk/scopes/_normalize.py @@ -3,7 +3,6 @@ import typing as t from .representation import Scope -from .scope_definition import MutableScope if t.TYPE_CHECKING: from globus_sdk._types import ScopeCollectionType @@ -22,7 +21,7 @@ def scopes_to_str(scopes: ScopeCollectionType) -> str: >>> scopes_to_str(Scope("foo")) 'foo' - >>> scopes_to_str(Scope("foo"), "bar", MutableScope("qux")) + >>> scopes_to_str(Scope("foo"), "bar", Scope("qux")) 'foo bar qux' """ scope_iter = _iter_scope_collection(scopes, split_root_scopes=False) @@ -42,15 +41,13 @@ def scopes_to_scope_list(scopes: ScopeCollectionType) -> list[Scope]: >>> scopes_to_scope_list(Scope("foo")) [Scope('foo')] - >>> scopes_to_scope_list(Scope("foo"), "bar baz", MutableScope("qux")) + >>> scopes_to_scope_list(Scope("foo"), "bar baz", Scope("qux")) [Scope('foo'), Scope('bar'), Scope('baz'), Scope('qux')] """ scope_list: list[Scope] = [] for scope in _iter_scope_collection(scopes): if isinstance(scope, str): scope_list.extend(Scope.parse(scope)) - elif isinstance(scope, MutableScope): - scope_list.extend(Scope.parse(str(scope))) else: scope_list.append(scope) return scope_list @@ -60,7 +57,7 @@ def _iter_scope_collection( obj: ScopeCollectionType, *, split_root_scopes: bool = True, -) -> t.Iterator[str | MutableScope | Scope]: +) -> t.Iterator[str | Scope]: """ Provide an iterator over a scope collection type, flattening nested scope collections as encountered. @@ -88,7 +85,7 @@ def _iter_scope_collection( """ if isinstance(obj, str): yield from _iter_scope_string(obj, split_root_scopes) - elif isinstance(obj, MutableScope) or isinstance(obj, Scope): + elif isinstance(obj, Scope): yield obj else: for item in obj: diff --git a/src/globus_sdk/scopes/builder.py b/src/globus_sdk/scopes/builder.py index c74ae30dc..a645eaa4c 100644 --- a/src/globus_sdk/scopes/builder.py +++ b/src/globus_sdk/scopes/builder.py @@ -2,8 +2,6 @@ import typing as t -from .scope_definition import MutableScope - ScopeBuilderScopes = t.Union[ None, str, @@ -126,34 +124,6 @@ def url_scope_string(self, scope_name: str) -> str: """ return f"https://auth.globus.org/scopes/{self.resource_server}/{scope_name}" - def make_mutable(self, scope: str, *, optional: bool = False) -> MutableScope: - """ - For a given scope, create a MutableScope object. - - The ``scope`` name given refers to the name of a scope attached to the - ScopeBuilder. It is given by attribute name, not by the full scope string. - - **Examples** - - Using the ``TransferScopes`` object, one could reference ``all`` as follows: - - >>> TransferScopes.all - 'urn:globus:auth:scope:transfer.api.globus.org:all' - >>> TransferScopes.make_mutable("all") - Scope('urn:globus:auth:scope:transfer.api.globus.org:all') - - This is equivalent to constructing a Scope object from the resolved - scope string, as in - - >>> Scope(TransferScopes.all) - Scope('urn:globus:auth:scope:transfer.api.globus.org:all') - - :param scope: The name of the scope to convert to a MutableScope - :param optional: If true, the created MutableScope object will be marked - optional - """ - return MutableScope(getattr(self, scope), optional=optional) - def __str__(self) -> str: return f"{self.__class__.__name__}[{self.resource_server}]\n" + "\n".join( f" {name}:\n {getattr(self, name)}" for name in self.scope_names diff --git a/src/globus_sdk/scopes/scope_definition.py b/src/globus_sdk/scopes/scope_definition.py deleted file mode 100644 index 7d9d677c9..000000000 --- a/src/globus_sdk/scopes/scope_definition.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -THIS IS A LEGACY MODULE - -This module defines a legacy scope object and parser called `MutableScope`. -It is maintained for backwards compatibility. - -For new code, use the `globus_sdk.Scope` object. -""" - -from __future__ import annotations - -import typing as t -import warnings - -if t.TYPE_CHECKING: - from globus_sdk._types import ScopeCollectionType - - -class MutableScope: - """ - A scope object is a representation of a scope which allows modifications to be - made. In particular, it supports handling scope dependencies via - ``add_dependency``. - - `str(MutableScope(...))` produces a valid scope string for use in various methods. - - :param scope_string: The string which will be used as the basis for this Scope - :param optional: The scope may be marked as optional. This means that the scope can - be declined by the user without declining consent for other scopes - """ - - def __init__( - self, - scope_string: str, - *, - optional: bool = False, - dependencies: list[MutableScope] | None = None, - ) -> None: - if any(c in scope_string for c in "[]* "): - raise ValueError( - "MutableScope instances may not contain the special characters '[]* '." - ) - self.scope_string = scope_string - self.optional = optional - self.dependencies: list[MutableScope] = ( - [] if dependencies is None else dependencies - ) - - def serialize(self) -> str: - base_scope = ("*" if self.optional else "") + self.scope_string - if not self.dependencies: - return base_scope - return ( - base_scope + "[" + " ".join(c.serialize() for c in self.dependencies) + "]" - ) - - def add_dependency( - self, - scope: str | MutableScope, - *, - optional: bool | None = None, - ) -> MutableScope: - """ - Add a scope dependency. The dependent scope relationship will be stored in the - Scope and will be evident in its string representation. - - :param scope: The scope upon which the current scope depends - :param optional: Mark the dependency an optional one. By default it is not. An - optional scope dependency can be declined by the user without declining - consent for the primary scope - """ - if optional is not None: - if isinstance(scope, MutableScope): - raise ValueError( - "cannot use optional=... with a MutableScope object as the " - "argument to add_dependency" - ) - warnings.warn( - "Passing 'optional' to add_dependency is deprecated. " - "Construct an optional MutableScope object instead.", - DeprecationWarning, - stacklevel=2, - ) - scopeobj = MutableScope(scope, optional=optional) - else: - if isinstance(scope, str): - scopeobj = MutableScope(scope) - else: - scopeobj = scope - self.dependencies.append(scopeobj) - return self - - def __repr__(self) -> str: - parts: list[str] = [f"'{self.scope_string}'"] - if self.optional: - parts.append("optional=True") - if self.dependencies: - parts.append(f"dependencies={self.dependencies!r}") - return "MutableScope(" + ", ".join(parts) + ")" - - def __str__(self) -> str: - return self.serialize() - - @staticmethod - def scopes2str(obj: ScopeCollectionType) -> str: - """ - .. warning:: - - Deprecated. Prefer ``globus_sdk.scopes.scopes_to_str``. - - Given a scope string, a collection of scope strings, a MutableScope object, a - collection of MutableScope objects, or a mixed collection of strings and - Scopes, convert to a string which can be used in a request. - - :param obj: The object or collection to convert to a string - """ - from ._normalize import scopes_to_str - - return scopes_to_str(obj) diff --git a/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py b/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py index dbdac64f2..20faf6ebb 100644 --- a/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py +++ b/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py @@ -1,7 +1,7 @@ import urllib.parse from globus_sdk._testing import get_last_request, load_response -from globus_sdk.scopes import MutableScope +from globus_sdk.scopes import Scope def test_oauth2_client_credentials_tokens(auth_client): @@ -14,10 +14,10 @@ def test_oauth2_client_credentials_tokens(auth_client): ) -def test_oauth2_client_credentials_tokens_can_accept_mutable_scope_object(auth_client): +def test_oauth2_client_credentials_tokens_can_accept_scope_object(auth_client): meta = load_response(auth_client.oauth2_client_credentials_tokens).metadata - response = auth_client.oauth2_client_credentials_tokens(MutableScope(meta["scope"])) + response = auth_client.oauth2_client_credentials_tokens(Scope(meta["scope"])) assert ( response.by_resource_server[meta["resource_server"]]["access_token"] == meta["access_token"] diff --git a/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py b/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py index f925e6447..a82235dab 100644 --- a/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py +++ b/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py @@ -1,6 +1,6 @@ import globus_sdk from globus_sdk._types import ScopeCollectionType -from globus_sdk.scopes import MutableScope, scopes_to_str +from globus_sdk.scopes import Scope, scopes_to_str from globus_sdk.services.auth import ( GlobusAuthorizationCodeFlowManager, GlobusNativeAppFlowManager, @@ -21,20 +21,16 @@ ) -# these functions should type-check okay +# this function should type-check okay def foo(x: ScopeCollectionType) -> str: - return MutableScope.scopes2str(x) - - -def foo2(x: ScopeCollectionType) -> str: return scopes_to_str(x) foo("somestring") foo(["somestring", "otherstring"]) -foo(MutableScope("bar")) -foo((MutableScope("bar"),)) -foo({MutableScope("bar"), "baz"}) +foo(Scope("bar")) +foo((Scope("bar"),)) +foo({Scope("bar"), "baz"}) # bad usages foo(1) # type: ignore[arg-type] foo((False,)) # type: ignore[arg-type] @@ -62,29 +58,29 @@ def foo2(x: ScopeCollectionType) -> str: GlobusAuthorizationCodeFlowManager( cc_client, "https://example.org/redirect-uri", - requested_scopes=MutableScope("foo"), + requested_scopes=Scope("foo"), ) GlobusNativeAppFlowManager( native_client, - requested_scopes=MutableScope("foo"), + requested_scopes=Scope("foo"), ) GlobusAuthorizationCodeFlowManager( cc_client, "https://example.org/redirect-uri", - requested_scopes=[MutableScope("foo")], + requested_scopes=[Scope("foo")], ) GlobusNativeAppFlowManager( native_client, - requested_scopes=[MutableScope("foo")], + requested_scopes=[Scope("foo")], ) GlobusAuthorizationCodeFlowManager( cc_client, "https://example.org/redirect-uri", - requested_scopes=[MutableScope("foo"), "bar"], + requested_scopes=[Scope("foo"), "bar"], ) GlobusNativeAppFlowManager( native_client, - requested_scopes=[MutableScope("foo"), "bar"], + requested_scopes=[Scope("foo"), "bar"], ) # bad usages GlobusAuthorizationCodeFlowManager( @@ -120,19 +116,17 @@ def foo2(x: ScopeCollectionType) -> str: cc_client.oauth2_start_flow( "https://example.org/redirect-uri", requested_scopes=("foo", "bar") ) -native_client.oauth2_start_flow(MutableScope("foo")) -cc_client.oauth2_start_flow("https://example.org/redirect-uri", MutableScope("foo")) -native_client.oauth2_start_flow(requested_scopes=MutableScope("foo")) -cc_client.oauth2_start_flow( - "https://example.org/redirect-uri", requested_scopes=MutableScope("foo") -) -native_client.oauth2_start_flow([MutableScope("foo"), "bar"]) +native_client.oauth2_start_flow(Scope("foo")) +cc_client.oauth2_start_flow("https://example.org/redirect-uri", Scope("foo")) +native_client.oauth2_start_flow(requested_scopes=Scope("foo")) cc_client.oauth2_start_flow( - "https://example.org/redirect-uri", [MutableScope("foo"), "bar"] + "https://example.org/redirect-uri", requested_scopes=Scope("foo") ) -native_client.oauth2_start_flow(requested_scopes=[MutableScope("foo"), "bar"]) +native_client.oauth2_start_flow([Scope("foo"), "bar"]) +cc_client.oauth2_start_flow("https://example.org/redirect-uri", [Scope("foo"), "bar"]) +native_client.oauth2_start_flow(requested_scopes=[Scope("foo"), "bar"]) cc_client.oauth2_start_flow( - "https://example.org/redirect-uri", requested_scopes=[MutableScope("foo"), "bar"] + "https://example.org/redirect-uri", requested_scopes=[Scope("foo"), "bar"] ) # bad usages native_client.oauth2_start_flow(1) # type: ignore[arg-type] @@ -153,12 +147,10 @@ def foo2(x: ScopeCollectionType) -> str: cc_client.oauth2_client_credentials_tokens(requested_scopes="foo") cc_client.oauth2_client_credentials_tokens(("foo", "bar")) cc_client.oauth2_client_credentials_tokens(requested_scopes=("foo", "bar")) -cc_client.oauth2_client_credentials_tokens(MutableScope("foo")) -cc_client.oauth2_client_credentials_tokens(requested_scopes=MutableScope("foo")) -cc_client.oauth2_client_credentials_tokens([MutableScope("foo"), "bar"]) -cc_client.oauth2_client_credentials_tokens( - requested_scopes=[MutableScope("foo"), "bar"] -) +cc_client.oauth2_client_credentials_tokens(Scope("foo")) +cc_client.oauth2_client_credentials_tokens(requested_scopes=Scope("foo")) +cc_client.oauth2_client_credentials_tokens([Scope("foo"), "bar"]) +cc_client.oauth2_client_credentials_tokens(requested_scopes=[Scope("foo"), "bar"]) cc_client.oauth2_client_credentials_tokens(1) # type: ignore[arg-type] cc_client.oauth2_client_credentials_tokens( requested_scopes=none_list, # type: ignore[arg-type] diff --git a/tests/unit/authorizers/test_client_credentials_authorizer.py b/tests/unit/authorizers/test_client_credentials_authorizer.py index 528e8cca6..e7cd46227 100644 --- a/tests/unit/authorizers/test_client_credentials_authorizer.py +++ b/tests/unit/authorizers/test_client_credentials_authorizer.py @@ -3,7 +3,7 @@ import pytest from globus_sdk.authorizers import ClientCredentialsAuthorizer -from globus_sdk.scopes import MutableScope +from globus_sdk.scopes import Scope ACCESS_TOKEN = "access_token_1" EXPIRES_AT = -1 @@ -62,11 +62,11 @@ def test_multiple_resource_servers(authorizer, response): assert SCOPES in str(excinfo.value) -def test_can_create_authorizer_from_mutable_scopes(client): - a1 = ClientCredentialsAuthorizer(client, MutableScope("foo")) +def test_can_create_authorizer_from_scope_objects(client): + a1 = ClientCredentialsAuthorizer(client, Scope("foo")) assert a1.scopes == "foo" a2 = ClientCredentialsAuthorizer( - client, [MutableScope("foo"), "bar", MutableScope("baz").add_dependency("buzz")] + client, [Scope("foo"), "bar", Scope("baz").add_dependency("buzz")] ) assert a2.scopes == "foo bar baz[buzz]" diff --git a/tests/unit/helpers/test_auth_scope_stringify.py b/tests/unit/helpers/test_auth_scope_stringify.py index 09d3a77f9..d95622cb4 100644 --- a/tests/unit/helpers/test_auth_scope_stringify.py +++ b/tests/unit/helpers/test_auth_scope_stringify.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk.scopes import MutableScope +from globus_sdk.scopes import Scope from globus_sdk.services.auth._common import stringify_requested_scopes @@ -9,10 +9,10 @@ def test_scope_stringify_roundtrips_string(): assert stringify_requested_scopes("foo") == "foo" -def test_scope_stringify_matches_str_of_mutable_scope(): - foo_scope = MutableScope("foo") +def test_scope_stringify_matches_str_of_scope_object(): + foo_scope = Scope("foo") # these asserts are nearly equivalent, but not quite the same - # MutableScope.__str__ could -- at least, in theory -- change in the future + # Scope.__str__ could -- at least, in theory -- change in the future assert stringify_requested_scopes(foo_scope) == str(foo_scope) assert stringify_requested_scopes(foo_scope) == "foo" diff --git a/tests/unit/scopes/test_mutable_scope.py b/tests/unit/scopes/test_mutable_scope.py deleted file mode 100644 index 4e4e640bc..000000000 --- a/tests/unit/scopes/test_mutable_scope.py +++ /dev/null @@ -1,48 +0,0 @@ -import pytest - -from globus_sdk.scopes import MutableScope - - -def test_scope_str_and_repr_simple(): - s = MutableScope("simple") - assert str(s) == "simple" - assert repr(s) == "MutableScope('simple')" - - -def test_scope_str_and_repr_optional(): - s = MutableScope("simple", optional=True) - assert str(s) == "*simple" - assert repr(s) == "MutableScope('simple', optional=True)" - - -def test_scope_str_and_repr_with_dependencies(): - s = MutableScope("top") - s.add_dependency("foo") - assert str(s) == "top[foo]" - s.add_dependency("bar") - assert str(s) == "top[foo bar]" - assert ( - repr(s) == "MutableScope('top', " - "dependencies=[MutableScope('foo'), MutableScope('bar')])" - ) - - -def test_add_dependency_warns_on_optional_but_still_has_good_str_and_repr(): - s = MutableScope("top") - # this should warn, the use of `optional=...` rather than adding a Scope object - # when optional dependencies are wanted is deprecated - with pytest.warns(DeprecationWarning): - s.add_dependency("foo", optional=True) - - # confirm the str representation and repr for good measure - assert str(s) == "top[*foo]" - assert ( - repr(s) - == "MutableScope('top', dependencies=[MutableScope('foo', optional=True)])" - ) - - -@pytest.mark.parametrize("scope_str", ("*foo", "foo[bar]", "foo[", "foo]", "foo bar")) -def test_scope_init_forbids_special_chars(scope_str): - with pytest.raises(ValueError): - MutableScope(scope_str) diff --git a/tests/unit/scopes/test_scope_builder.py b/tests/unit/scopes/test_scope_builder.py index b9a37177d..37d078c8b 100644 --- a/tests/unit/scopes/test_scope_builder.py +++ b/tests/unit/scopes/test_scope_builder.py @@ -77,17 +77,6 @@ def test_sb_allowed_inputs_types(): assert list_sb.do_a_thing == scope_1_urn -def test_scopebuilder_make_mutable_produces_same_strings(): - sb = ScopeBuilder(str(uuid.UUID(int=0)), known_scopes="foo", known_url_scopes="bar") - assert str(sb.make_mutable("foo")) == sb.foo - assert str(sb.make_mutable("bar")) == sb.bar - - -def test_scopebuilder_make_mutable_can_be_optional(): - sb = ScopeBuilder(str(uuid.UUID(int=0)), known_scopes="foo") - assert str(sb.make_mutable("foo", optional=True)) == "*" + sb.foo - - def test_flows_scopes_creation(): assert FlowsScopes.resource_server == "flows.globus.org" assert ( diff --git a/tests/unit/scopes/test_scope_normalization.py b/tests/unit/scopes/test_scope_normalization.py index 6f697f33b..afde75fbb 100644 --- a/tests/unit/scopes/test_scope_normalization.py +++ b/tests/unit/scopes/test_scope_normalization.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk.scopes import MutableScope, Scope, scopes_to_scope_list, scopes_to_str +from globus_sdk.scopes import Scope, scopes_to_scope_list, scopes_to_str def test_scopes_to_str_roundtrip_simple_str(): @@ -28,9 +28,8 @@ def test_scopes_to_str_roundtrip_simple_str_in_collection(scope_collection): "scope_collection, expect_str", ( (("scope1", Scope("scope2")), "scope1 scope2"), - (("scope1", MutableScope("scope2")), "scope1 scope2"), - ((Scope("scope1"), MutableScope("scope2")), "scope1 scope2"), - ((Scope("scope1"), MutableScope("scope2"), "scope3"), "scope1 scope2 scope3"), + ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), + ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), ( ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), "scope1 scope2 scope3 scope4", @@ -44,7 +43,7 @@ def test_scopes_to_str_handles_mixed_data(scope_collection, expect_str): @pytest.mark.parametrize( "scope_collection", - ([Scope("scope1")], Scope("scope1"), "scope1", MutableScope("scope1")), + ([Scope("scope1")], Scope("scope1"), "scope1"), ) def test_scopes_to_scope_list_simple(scope_collection): actual_list = scopes_to_scope_list(scope_collection) @@ -58,9 +57,9 @@ def test_scopes_to_scope_list_simple(scope_collection): "scope_collection, expect_str", ( (("scope1", "scope2"), "scope1 scope2"), - (("scope1", MutableScope("scope2")), "scope1 scope2"), - ((Scope("scope1"), MutableScope("scope2")), "scope1 scope2"), - ((Scope("scope1"), MutableScope("scope2"), "scope3"), "scope1 scope2 scope3"), + (("scope1", Scope("scope2")), "scope1 scope2"), + ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), + ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), ( ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), "scope1 scope2 scope3 scope4", From 4495a791f577236e4ddfe4aa6d8b0495cc2f5f54 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 28 May 2025 19:24:02 -0500 Subject: [PATCH 011/176] Add an entry to the upgrading doc for MutableScope This is a small/minimal entry so that we're guaranteed to have something to build upon. It intentionally does not go into detail on how to use `Scope` because these interfaces are being redesigned. --- docs/upgrading.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7b1784910..80195bd92 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -45,6 +45,31 @@ Then, code can dispatch with From 3.x to 4.0 --------------- +``MutableScope`` is Removed, use ``Scope`` Instead +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``MutableScope`` type was removed in version 4 in favor of the ``Scope`` +type. +When manipulating scopes as objects, use the ``Scope`` object anywhere that +``MutableScope`` was used, for example: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.scopes import MutableScope + + my_scope = MutableScope("urn:globus:auth:scopes:transfer.api.globus.org:all") + + # globus-sdk v4 + from globus_sdk.scopes import Scope + + my_scope = Scope("urn:globus:auth:scopes:transfer.api.globus.org:all") + +.. note:: + + The ``Scope`` type was added in Globus SDK v3, so this transition can be + made prior to upgrading to version 4. + ``requested_scopes`` is Required ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From aa4a7af7c4612c14caafa4ac05a8fed2b6bce373 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 29 May 2025 11:38:11 -0500 Subject: [PATCH 012/176] Refine upgrade doc on MutableScope Fix some phrasing and link to classes. Co-authored-by: Kurt McKee <39996+kurtmckee@users.noreply.github.com> --- docs/upgrading.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 80195bd92..9e9a57f38 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -48,9 +48,10 @@ From 3.x to 4.0 ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``MutableScope`` type was removed in version 4 in favor of the ``Scope`` -type. -When manipulating scopes as objects, use the ``Scope`` object anywhere that +The ``MutableScope`` type was removed in version 4 in favor of the +:class:`Scope ` type. +When manipulating scopes as objects, use +:class:`Scope ` anywhere that ``MutableScope`` was used, for example: .. code-block:: python @@ -67,8 +68,8 @@ When manipulating scopes as objects, use the ``Scope`` object anywhere that .. note:: - The ``Scope`` type was added in Globus SDK v3, so this transition can be - made prior to upgrading to version 4. + The :class:`Scope ` type was added in Globus SDK + v3, so this transition can be made prior to upgrading to version 4. ``requested_scopes`` is Required ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 92b9f51576efc332c959848d5331f18af1e35f5d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 29 May 2025 12:27:51 -0500 Subject: [PATCH 013/176] Remove legacy "GARE" module aliasing - Remove `globus_sdk.experimental.auth_requirements_error` - Remove a test which checks the aliasing - Add an upgrading doc section on removal of experimental aliases --- ...22204_sirosen_remove_legacy_gare_alias.rst | 5 ++ docs/upgrading.rst | 18 +++++++ .../experimental/auth_requirements_error.py | 51 ------------------- .../unit/experimental/test_legacy_support.py | 29 ----------- 4 files changed, 23 insertions(+), 80 deletions(-) create mode 100644 changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst delete mode 100644 src/globus_sdk/experimental/auth_requirements_error.py diff --git a/changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst b/changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst new file mode 100644 index 000000000..6685372f9 --- /dev/null +++ b/changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst @@ -0,0 +1,5 @@ +Removed +~~~~~~~ + +- ``globus_sdk.experimental.auth_requirements_error`` has been removed. Use + ``globus_sdk.gare`` instead. (:pr:`1202`) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 9e9a57f38..52bada59e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -45,6 +45,24 @@ Then, code can dispatch with From 3.x to 4.0 --------------- +Deprecated Experimental Aliases Removed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +During the version 3 lifecycle, several modules were added under +``globus_sdk.experimental`` and later promoted to new names in the main +``globus_sdk`` namespace. +Compatibility aliases were left in place. + +Under version 4, the compatibility aliases have been removed. +The removed alias and new module names are shown in the table below. + +=================================================== =================== +Removed Alias New Name +=================================================== =================== +``globus_sdk.experimental.auth_requirements_error`` ``globus_sdk.gare`` +=================================================== =================== + + ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/globus_sdk/experimental/auth_requirements_error.py b/src/globus_sdk/experimental/auth_requirements_error.py deleted file mode 100644 index 3a6521de9..000000000 --- a/src/globus_sdk/experimental/auth_requirements_error.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -import sys -import typing as t - -__all__ = ( - "GlobusAuthRequirementsError", - "GlobusAuthorizationParameters", - "to_auth_requirements_error", - "to_auth_requirements_errors", - "is_auth_requirements_error", - "has_auth_requirements_errors", -) - -# legacy aliases -# (when accessed, these will emit deprecation warnings) -if t.TYPE_CHECKING: - from globus_sdk.gare import GARE as GlobusAuthRequirementsError - from globus_sdk.gare import GlobusAuthorizationParameters - from globus_sdk.gare import has_gares as has_auth_requirements_errors - from globus_sdk.gare import is_gare as is_auth_requirements_error - from globus_sdk.gare import to_gare as to_auth_requirements_error - from globus_sdk.gare import to_gares as to_auth_requirements_errors -else: - - _RENAMES = { - "GlobusAuthRequirementsError": "GARE", - "to_auth_requirements_error": "to_gare", - "to_auth_requirements_errors": "to_gares", - "is_auth_requirements_error": "is_gare", - "has_auth_requirements_errors": "has_gares", - } - - def __getattr__(name: str) -> t.Any: - import globus_sdk.gare as gare_module - from globus_sdk.exc import warn_deprecated - - new_name = _RENAMES.get(name, name) - - warn_deprecated( - "'globus_sdk.experimental.auth_requirements_error' has been renamed to " - "'globus_sdk.gare'. " - f"Importing '{name}' from `globus_sdk.experimental` is deprecated. " - f"Use `globus_sdk.gare.{new_name}` instead." - ) - - value = getattr(gare_module, new_name, None) - if value is None: - raise AttributeError(f"module {__name__} has no attribute {name}") - setattr(sys.modules[__name__], name, value) - return value diff --git a/tests/unit/experimental/test_legacy_support.py b/tests/unit/experimental/test_legacy_support.py index 9d348b392..4fef25829 100644 --- a/tests/unit/experimental/test_legacy_support.py +++ b/tests/unit/experimental/test_legacy_support.py @@ -12,14 +12,6 @@ import pytest from globus_sdk import RemovedInV4Warning -from globus_sdk.gare import ( - GARE, - GlobusAuthorizationParameters, - has_gares, - is_gare, - to_gare, - to_gares, -) def test_scope_importable_from_experimental(): @@ -30,27 +22,6 @@ def test_scope_importable_from_experimental(): ) -@pytest.mark.parametrize( - "alias, value", - ( - ("GlobusAuthorizationParameters", GlobusAuthorizationParameters), - ("GlobusAuthRequirementsError", GARE), - ("to_auth_requirements_error", to_gare), - ("to_auth_requirements_errors", to_gares), - ("has_auth_requirements_errors", has_gares), - ("is_auth_requirements_error", is_gare), - ), -) -def test_deprecated_experimental_alias(alias, value): - with pytest.warns(RemovedInV4Warning): - from globus_sdk.experimental import ( - auth_requirements_error as experimental_module, - ) - - aliased_value = getattr(experimental_module, alias) - assert aliased_value is value - - def test_login_flow_manager_importable_from_experimental(): with pytest.warns(RemovedInV4Warning): from globus_sdk.experimental.login_flow_manager import ( # noqa: F401 From f307e06f8c1d41e1f1079844115cc88c4b577840 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 29 May 2025 13:05:42 -0500 Subject: [PATCH 014/176] Convert to a csv-table in docs Co-authored-by: Kurt McKee <39996+kurtmckee@users.noreply.github.com> --- docs/upgrading.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 52bada59e..042eef493 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -56,12 +56,10 @@ Compatibility aliases were left in place. Under version 4, the compatibility aliases have been removed. The removed alias and new module names are shown in the table below. -=================================================== =================== -Removed Alias New Name -=================================================== =================== -``globus_sdk.experimental.auth_requirements_error`` ``globus_sdk.gare`` -=================================================== =================== +.. csv-table:: + :header: "Removed alias", "New name" + "``globus_sdk.experimental.auth_requirements_error``", "``globus_sdk.gare``" ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 86c8587ceb140e58a6c489a278f421b5ca670d93 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 30 May 2025 11:10:13 -0500 Subject: [PATCH 015/176] Remove the setter for GlobusAPIError.message (#1204) This was deprecated under 3.x and is now being removed. It is sufficiently minor that it probably doesn't warrant a call-out in the upgrading guide doc. --- ...irosen_remove_deprecated_message_setter.rst | 5 +++++ src/globus_sdk/exc/api.py | 8 -------- tests/unit/errors/test_common_functionality.py | 18 ------------------ 3 files changed, 5 insertions(+), 26 deletions(-) create mode 100644 changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst diff --git a/changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst b/changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst new file mode 100644 index 000000000..4daa62886 --- /dev/null +++ b/changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst @@ -0,0 +1,5 @@ +Removed +~~~~~~~ + +- ``GlobusAPIError`` no longer provides a setter for ``message``. The + ``message`` property is now read-only. (:pr:`NUMBER`) diff --git a/src/globus_sdk/exc/api.py b/src/globus_sdk/exc/api.py index 9839ff608..93c7c904c 100644 --- a/src/globus_sdk/exc/api.py +++ b/src/globus_sdk/exc/api.py @@ -68,14 +68,6 @@ def message(self) -> str | None: return "; ".join(self.messages) return None - @message.setter - def message(self, value: str) -> None: - warn_deprecated( - "Setting a message on GlobusAPIError objects is deprecated. " - "This overwrites any parsed messages. Append to 'messages' instead." - ) - self.messages = [value] - @property def http_reason(self) -> str: """ diff --git a/tests/unit/errors/test_common_functionality.py b/tests/unit/errors/test_common_functionality.py index 775418dd6..95997229e 100644 --- a/tests/unit/errors/test_common_functionality.py +++ b/tests/unit/errors/test_common_functionality.py @@ -65,24 +65,6 @@ def test_raw_text_property_warns(): assert err.raw_text == body_text -def test_imperative_message_setting_warns(): - err = construct_error( - body={"code": "FooCode", "message": "FooMessage"}, http_status=400 - ) - assert err.message == "FooMessage" - - with pytest.warns( - RemovedInV4Warning, - match=( - r"Setting a message on GlobusAPIError objects is deprecated\. " - r"This overwrites any parsed messages\. Append to 'messages' instead\." - ), - ): - err.message = "BarMessage" - - assert err.message == "BarMessage" - - @pytest.mark.parametrize( "body, response_headers, http_status, expect_code, expect_message", ( From ed8fc5e36874db5adcfb4084ce8d8c9b2954103e Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Mon, 2 Jun 2025 15:07:04 -0500 Subject: [PATCH 016/176] Update flows client classes to use MISSING defaults (#1205) * Updated flows client to use MISSING defaults * Added changelog * Requested changes: removed missing from additional_fields and query_params * Requested changes: added back orderby comment --- ...tuecke_sc_15807_flows_missing_defaults.rst | 4 + src/globus_sdk/services/flows/client.py | 264 ++++++++---------- src/globus_sdk/utils.py | 4 +- .../services/flows/test_flow_crud.py | 16 +- .../services/flows/test_flow_validate.py | 5 +- .../functional/services/flows/test_get_run.py | 5 +- .../services/flows/test_list_flows.py | 11 +- .../services/flows/test_list_runs.py | 3 +- 8 files changed, 142 insertions(+), 170 deletions(-) create mode 100644 changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst diff --git a/changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst b/changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst new file mode 100644 index 000000000..b064c4c9f --- /dev/null +++ b/changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst @@ -0,0 +1,4 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- All defaults of None converted to globus_sdk.MISSING for all payload types in the flows client. (:pr:`1205`) \ No newline at end of file diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 87fd61f1f..4663aefd3 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -47,15 +47,15 @@ def create_flow( title: str, definition: dict[str, t.Any], input_schema: dict[str, t.Any], - subtitle: str | None = None, - description: str | None = None, - flow_viewers: list[str] | None = None, - flow_starters: list[str] | None = None, - flow_administrators: list[str] | None = None, - run_managers: list[str] | None = None, - run_monitors: list[str] | None = None, - keywords: list[str] | None = None, - subscription_id: UUIDLike | None = None, + subtitle: str | MissingType = MISSING, + description: str | MissingType = MISSING, + flow_viewers: list[str] | MissingType = MISSING, + flow_starters: list[str] | MissingType = MISSING, + flow_administrators: list[str] | MissingType = MISSING, + run_managers: list[str] | MissingType = MISSING, + run_monitors: list[str] | MissingType = MISSING, + keywords: list[str] | MissingType = MISSING, + subscription_id: UUIDLike | None | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ @@ -186,25 +186,20 @@ def create_flow( """ # noqa E501 data = { - k: v - for k, v in { - "title": title, - "definition": definition, - "input_schema": input_schema, - "subtitle": subtitle, - "description": description, - "flow_viewers": flow_viewers, - "flow_starters": flow_starters, - "flow_administrators": flow_administrators, - "run_managers": run_managers, - "run_monitors": run_monitors, - "keywords": keywords, - "subscription_id": subscription_id, - }.items() - if v is not None + "title": title, + "definition": definition, + "input_schema": input_schema, + "subtitle": subtitle, + "description": description, + "flow_viewers": flow_viewers, + "flow_starters": flow_starters, + "flow_administrators": flow_administrators, + "run_managers": run_managers, + "run_monitors": run_monitors, + "keywords": keywords, + "subscription_id": subscription_id, + **(additional_fields or {}), } - data.update(additional_fields or {}) - return self.post("/flows", data=data) def get_flow( @@ -227,21 +222,17 @@ def get_flow( :service: flows :ref: Flows/paths/~1flows~1{flow_id}/get """ - - if query_params is None: - query_params = {} - return self.get(f"/flows/{flow_id}", query_params=query_params) @paging.has_paginator(paging.MarkerPaginator, items_key="flows") def list_flows( self, *, - filter_role: str | None = None, - filter_roles: str | t.Iterable[str] | None = None, - filter_fulltext: str | None = None, - orderby: str | t.Iterable[str] | None = None, - marker: str | None = None, + filter_role: str | MissingType = MISSING, + filter_roles: str | t.Iterable[str] | MissingType = MISSING, + filter_fulltext: str | MissingType = MISSING, + orderby: str | t.Iterable[str] | MissingType = MISSING, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableFlowsResponse: """ @@ -344,53 +335,45 @@ def list_flows( :service: flows :ref: Flows/paths/~1flows/get """ - - if query_params is None: - query_params = {} - if filter_role is not None: + if not isinstance(filter_role, MissingType): exc.warn_deprecated( "The `filter_role` parameter is deprecated. Use `filter_roles` instead." ) - query_params["filter_role"] = filter_role - if filter_roles is not None: - query_params["filter_roles"] = utils.commajoin(filter_roles) - if filter_fulltext is not None: - query_params["filter_fulltext"] = filter_fulltext - - if filter_role is not None and filter_roles is not None: + if not isinstance(filter_role, MissingType) and not isinstance( + filter_roles, MissingType + ): msg = "Mutually exclusive parameters: filter_role and filter_roles." raise GlobusSDKUsageError(msg) - - if orderby is not None: - if isinstance(orderby, str): - query_params["orderby"] = orderby - else: - # copy any input sequence to force the type to `list` which is known to - # behave well - # this also ensures that we will consume non-sequence iterables - # (e.g. generator expressions) in a well-defined way - query_params["orderby"] = list(orderby) - if marker is not None: - query_params["marker"] = marker - + query_params = { + "filter_role": filter_role, + "filter_roles": utils.commajoin(filter_roles), + "filter_fulltext": filter_fulltext, + # if `orderby` is an iterable (e.g., generator expression), it gets + # converted to a list in this step + "orderby": ( + orderby if isinstance(orderby, (str, MissingType)) else list(orderby) + ), + "marker": marker, + **(query_params or {}), + } return IterableFlowsResponse(self.get("/flows", query_params=query_params)) def update_flow( self, flow_id: UUIDLike, *, - title: str | None = None, - definition: dict[str, t.Any] | None = None, - input_schema: dict[str, t.Any] | None = None, - subtitle: str | None = None, - description: str | None = None, - flow_owner: str | None = None, - flow_viewers: list[str] | None = None, - flow_starters: list[str] | None = None, - flow_administrators: list[str] | None = None, - run_managers: list[str] | None = None, - run_monitors: list[str] | None = None, - keywords: list[str] | None = None, + title: str | MissingType = MISSING, + definition: dict[str, t.Any] | MissingType = MISSING, + input_schema: dict[str, t.Any] | MissingType = MISSING, + subtitle: str | MissingType = MISSING, + description: str | MissingType = MISSING, + flow_owner: str | MissingType = MISSING, + flow_viewers: list[str] | MissingType = MISSING, + flow_starters: list[str] | MissingType = MISSING, + flow_administrators: list[str] | MissingType = MISSING, + run_managers: list[str] | MissingType = MISSING, + run_monitors: list[str] | MissingType = MISSING, + keywords: list[str] | MissingType = MISSING, subscription_id: UUIDLike | t.Literal["DEFAULT"] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: @@ -514,26 +497,21 @@ def update_flow( """ # noqa E501 data = { - k: v - for k, v in { - "title": title, - "definition": definition, - "input_schema": input_schema, - "subtitle": subtitle, - "description": description, - "flow_owner": flow_owner, - "flow_viewers": flow_viewers, - "flow_starters": flow_starters, - "flow_administrators": flow_administrators, - "run_managers": run_managers, - "run_monitors": run_monitors, - "keywords": keywords, - "subscription_id": subscription_id, - }.items() - if v is not None and v is not MISSING + "title": title, + "definition": definition, + "input_schema": input_schema, + "subtitle": subtitle, + "description": description, + "flow_owner": flow_owner, + "flow_viewers": flow_viewers, + "flow_starters": flow_starters, + "flow_administrators": flow_administrators, + "run_managers": run_managers, + "run_monitors": run_monitors, + "keywords": keywords, + "subscription_id": subscription_id, + **(additional_fields or {}), } - data.update(additional_fields or {}) - return self.put(f"/flows/{flow_id}", data=data) def delete_flow( @@ -556,10 +534,6 @@ def delete_flow( :service: flows :ref: Flows/paths/~1flows~1{flow_id}/delete """ - - if query_params is None: - query_params = {} - return self.delete(f"/flows/{flow_id}", query_params=query_params) def validate_flow( @@ -611,16 +585,19 @@ def validate_flow( :ref: Flows/paths/~1flows~1validate/post """ # noqa E501 - data = {"definition": definition, "input_schema": input_schema} + data = { + "definition": definition, + "input_schema": input_schema, + } return self.post("/flows/validate", data=data) @paging.has_paginator(paging.MarkerPaginator, items_key="runs") def list_runs( self, *, - filter_flow_id: t.Iterable[UUIDLike] | UUIDLike | None = None, - filter_roles: str | t.Iterable[str] | None = None, - marker: str | None = None, + filter_flow_id: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, + filter_roles: str | t.Iterable[str] | MissingType = MISSING, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableRunsResponse: """ @@ -661,16 +638,12 @@ def list_runs( :service: flows :ref: Runs/paths/~1runs/get """ - if query_params is None: - query_params = {} - if filter_flow_id is not None: - query_params["filter_flow_id"] = ",".join( - utils.safe_strseq_iter(filter_flow_id) - ) - if filter_roles: - query_params["filter_roles"] = utils.commajoin(filter_roles) - if marker is not None: - query_params["marker"] = marker + query_params = { + "filter_flow_id": utils.commajoin(filter_flow_id), + "filter_roles": utils.commajoin(filter_roles), + "marker": marker, + **(query_params or {}), + } return IterableRunsResponse(self.get("/runs", query_params=query_params)) @paging.has_paginator(paging.MarkerPaginator, items_key="entries") @@ -678,9 +651,9 @@ def get_run_logs( self, run_id: UUIDLike, *, - limit: int | None = None, - reverse_order: bool | None = None, - marker: str | None = None, + limit: int | MissingType = MISSING, + reverse_order: bool | MissingType = MISSING, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableRunLogsResponse: """ @@ -719,8 +692,6 @@ def get_run_logs( "marker": marker, **(query_params or {}), } - # Filter out request keys with None values to allow server defaults - query_params = {k: v for k, v in query_params.items() if v is not None} return IterableRunLogsResponse( self.get(f"/runs/{run_id}/log", query_params=query_params) ) @@ -729,7 +700,7 @@ def get_run( self, run_id: UUIDLike, *, - include_flow_description: bool | None = None, + include_flow_description: bool | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ @@ -763,11 +734,10 @@ def get_run( :service: flows :ref: Flows/paths/~1runs~1{run_id}/get """ - - query_params = query_params or {} - if include_flow_description is not None: - query_params["include_flow_description"] = include_flow_description - + query_params = { + "include_flow_description": include_flow_description, + **(query_params or {}), + } return self.get(f"/runs/{run_id}", query_params=query_params) def get_run_definition( @@ -838,10 +808,10 @@ def update_run( self, run_id: UUIDLike, *, - label: str | None = None, - tags: list[str] | None = None, - run_monitors: list[str] | None = None, - run_managers: list[str] | None = None, + label: str | MissingType = MISSING, + tags: list[str] | MissingType = MISSING, + run_monitors: list[str] | MissingType = MISSING, + run_managers: list[str] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ @@ -886,17 +856,12 @@ def update_run( """ data = { - k: v - for k, v in { - "tags": tags, - "label": label, - "run_monitors": run_monitors, - "run_managers": run_managers, - }.items() - if v is not None + "tags": tags, + "label": label, + "run_monitors": run_monitors, + "run_managers": run_managers, + **(additional_fields or {}), } - data.update(additional_fields or {}) - return self.put(f"/runs/{run_id}", data=data) def delete_run(self, run_id: UUIDLike) -> GlobusHTTPResponse: @@ -979,13 +944,13 @@ def run_flow( self, body: dict[str, t.Any], *, - label: str | None = None, - tags: list[str] | None = None, + label: str | MissingType = MISSING, + tags: list[str] | MissingType = MISSING, activity_notification_policy: ( - dict[str, t.Any] | RunActivityNotificationPolicy | None - ) = None, - run_monitors: list[str] | None = None, - run_managers: list[str] | None = None, + dict[str, t.Any] | RunActivityNotificationPolicy | MissingType + ) = MISSING, + run_monitors: list[str] | MissingType = MISSING, + run_managers: list[str] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ @@ -1014,19 +979,14 @@ def run_flow( :ref: ~1flows~1{flow_id}~1run/post """ data = { - k: v - for k, v in { - "body": body, - "tags": tags, - "label": label, - "activity_notification_policy": activity_notification_policy, - "run_monitors": run_monitors, - "run_managers": run_managers, - }.items() - if v is not None + "body": body, + "tags": tags, + "label": label, + "activity_notification_policy": activity_notification_policy, + "run_monitors": run_monitors, + "run_managers": run_managers, + **(additional_fields or {}), } - data.update(additional_fields or {}) - return self.post(f"/flows/{self._flow_id}/run", data=data) def resume_run(self, run_id: UUIDLike) -> GlobusHTTPResponse: diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index 3e5996085..08a1a214a 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -131,9 +131,11 @@ def safe_strseq_iter( yield str(x) -def commajoin(val: UUIDLike | t.Iterable[UUIDLike]) -> str: +def commajoin(val: UUIDLike | t.Iterable[UUIDLike] | MissingType) -> str | MissingType: # note that this explicit handling of Iterable allows for string-like objects to be # passed to this function and be stringified by the `str()` call + if isinstance(val, MissingType): + return val if isinstance(val, collections.abc.Iterable): return ",".join(safe_strseq_iter(val)) return str(val) diff --git a/tests/functional/services/flows/test_flow_crud.py b/tests/functional/services/flows/test_flow_crud.py index e0ebe3c80..7670c9820 100644 --- a/tests/functional/services/flows/test_flow_crud.py +++ b/tests/functional/services/flows/test_flow_crud.py @@ -6,9 +6,10 @@ from globus_sdk import FlowsAPIError from globus_sdk._testing import get_last_request, load_response from globus_sdk._testing.models import RegisteredResponse +from globus_sdk.utils import MISSING -@pytest.mark.parametrize("subscription_id", [None, "dummy_subscription_id"]) +@pytest.mark.parametrize("subscription_id", [MISSING, None, "dummy_subscription_id"]) def test_create_flow(flows_client, subscription_id): metadata = load_response(flows_client.create_flow).metadata @@ -19,13 +20,13 @@ def test_create_flow(flows_client, subscription_id): last_req = get_last_request() req_body = json.loads(last_req.body) - if subscription_id: + if subscription_id is not MISSING: assert req_body["subscription_id"] == subscription_id else: assert "subscription_id" not in req_body -@pytest.mark.parametrize("value", [None, [], ["dummy_value"]]) +@pytest.mark.parametrize("value", [MISSING, [], ["dummy_value"]]) @pytest.mark.parametrize("key", ["run_managers", "run_monitors"]) def test_create_flow_run_role_serialization(flows_client, key, value): @@ -40,7 +41,8 @@ def test_create_flow_run_role_serialization(flows_client, key, value): "input_schema": {}, } - request_body[key] = value + if value is not MISSING: + request_body[key] = value load_response( RegisteredResponse( @@ -66,7 +68,7 @@ def test_create_flow_run_role_serialization(flows_client, key, value): last_req = get_last_request() req_body = json.loads(last_req.body) - if value is None: + if value is MISSING: assert key not in req_body else: assert req_body[key] == value @@ -108,7 +110,7 @@ def test_update_flow(flows_client): assert resp[k] == v -@pytest.mark.parametrize("value", [None, [], ["dummy_value"]]) +@pytest.mark.parametrize("value", [MISSING, [], ["dummy_value"]]) @pytest.mark.parametrize("key", ["run_managers", "run_monitors"]) def test_update_flow_run_role_serialization(flows_client, key, value): metadata = load_response(flows_client.update_flow).metadata @@ -119,7 +121,7 @@ def test_update_flow_run_role_serialization(flows_client, key, value): last_req = get_last_request() req_body = json.loads(last_req.body) - if value is None: + if value is MISSING: assert key not in req_body else: assert req_body[key] == value diff --git a/tests/functional/services/flows/test_flow_validate.py b/tests/functional/services/flows/test_flow_validate.py index 4ff2a4d97..52f8172c0 100644 --- a/tests/functional/services/flows/test_flow_validate.py +++ b/tests/functional/services/flows/test_flow_validate.py @@ -4,15 +4,16 @@ from globus_sdk import FlowsAPIError from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING -@pytest.mark.parametrize("input_schema", [None, {}]) +@pytest.mark.parametrize("input_schema", [MISSING, {}]) def test_validate_flow(flows_client, input_schema): metadata = load_response(flows_client.validate_flow).metadata # Prepare the payload payload = {"definition": metadata["success"]} - if input_schema is not None: + if input_schema is not MISSING: payload["input_schema"] = input_schema resp = flows_client.validate_flow(**payload) diff --git a/tests/functional/services/flows/test_get_run.py b/tests/functional/services/flows/test_get_run.py index 4e745dee5..6324d540c 100644 --- a/tests/functional/services/flows/test_get_run.py +++ b/tests/functional/services/flows/test_get_run.py @@ -1,9 +1,10 @@ import pytest from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING -@pytest.mark.parametrize("include_flow_description", (None, False, True)) +@pytest.mark.parametrize("include_flow_description", (MISSING, False, True)) def test_get_run(flows_client, include_flow_description): metadata = load_response(flows_client.get_run).metadata @@ -14,7 +15,7 @@ def test_get_run(flows_client, include_flow_description): assert response.http_status == 200 request = get_last_request() - if include_flow_description is None: + if include_flow_description is MISSING: assert "flow_description" not in response assert "include_flow_description" not in request.url elif include_flow_description is False: diff --git a/tests/functional/services/flows/test_list_flows.py b/tests/functional/services/flows/test_list_flows.py index 8ba22ab4a..9b1c4bb39 100644 --- a/tests/functional/services/flows/test_list_flows.py +++ b/tests/functional/services/flows/test_list_flows.py @@ -4,11 +4,12 @@ from globus_sdk import GlobusSDKUsageError, RemovedInV4Warning from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING -@pytest.mark.parametrize("filter_fulltext", [None, "foo"]) -@pytest.mark.parametrize("filter_role", [None, "bar"]) -@pytest.mark.parametrize("orderby", [None, "created_at ASC"]) +@pytest.mark.parametrize("filter_fulltext", [MISSING, "foo"]) +@pytest.mark.parametrize("filter_role", [MISSING, "bar"]) +@pytest.mark.parametrize("orderby", [MISSING, "created_at ASC"]) def test_list_flows_simple(flows_client, filter_fulltext, filter_role, orderby): meta = load_response(flows_client.list_flows).metadata @@ -45,7 +46,7 @@ def test_list_flows_simple(flows_client, filter_fulltext, filter_role, orderby): ("filter_role", filter_role), ("orderby", orderby), ) - if v is not None + if v is not MISSING } assert parsed_qs == expect_query_params @@ -157,7 +158,7 @@ def test_list_flows_mutually_exclusive_roles(flows_client, filter_role, filter_r ([], None), ([""], None), (("",), None), - (None, None), + (MISSING, None), # single role as string ("foo", ["foo"]), # single role as list/tuple diff --git a/tests/functional/services/flows/test_list_runs.py b/tests/functional/services/flows/test_list_runs.py index 3be221aa0..8ecc7b51d 100644 --- a/tests/functional/services/flows/test_list_runs.py +++ b/tests/functional/services/flows/test_list_runs.py @@ -4,6 +4,7 @@ import pytest from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING def test_list_runs_simple(flows_client): @@ -79,7 +80,7 @@ def test_list_runs_filter_flow_id(flows_client, pass_as_uuids): ((), None), ([""], None), (("",), None), - (None, None), + (MISSING, None), # single role as string ("foo", ["foo"]), # single role as list/tuple From ca7f229ae03bca5430ff82f1e1dd3b12449169a8 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 3 Jun 2025 23:59:55 -0500 Subject: [PATCH 017/176] Remove deprecated aliases for "Timers" (#1206) Several objects named `Timer*` were exposed in the SDK as deprecated aliases for their `Timers*` renamed variants. These aliases are here removed. Also, delete the `globus_sdk.services.timer` module which aliased them. Tests and docs are updated to remove all reference to these old names. Co-authored-by: Kurt McKee <39996+kurtmckee@users.noreply.github.com> --- ...irosen_remove_deprecated_timer_aliases.rst | 5 ++ .../scopes_and_consents/scopes.rst | 4 -- docs/services/index.rst | 2 +- docs/services/{timer.rst => timers.rst} | 8 --- docs/upgrading.rst | 9 +++ scripts/ensure_exports_are_documented.py | 3 - src/globus_sdk/__init__.pyi | 3 - src/globus_sdk/scopes/__init__.py | 20 ------- src/globus_sdk/services/timer.py | 41 ------------- .../timer_client_aliasing.py | 12 ---- .../unit/services/timers/test_legacy_names.py | 60 ------------------- 11 files changed, 15 insertions(+), 152 deletions(-) create mode 100644 changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst rename docs/services/{timer.rst => timers.rst} (85%) delete mode 100644 src/globus_sdk/services/timer.py delete mode 100644 tests/non-pytest/mypy-ignore-tests/timer_client_aliasing.py delete mode 100644 tests/unit/services/timers/test_legacy_names.py diff --git a/changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst b/changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst new file mode 100644 index 000000000..3383cbd50 --- /dev/null +++ b/changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst @@ -0,0 +1,5 @@ +Removed +~~~~~~~ + +- Deprecated aliases for ``TimersClient``, ``TimersScopes``, and + ``TimersAPIError`` have been removed. (:pr:`NUMBER`) diff --git a/docs/authorization/scopes_and_consents/scopes.rst b/docs/authorization/scopes_and_consents/scopes.rst index 2d6687338..193185fe0 100644 --- a/docs/authorization/scopes_and_consents/scopes.rst +++ b/docs/authorization/scopes_and_consents/scopes.rst @@ -282,10 +282,6 @@ ScopeBuilder Constants .. listknownscopes:: globus_sdk.scopes.TimersScopes - .. note:: - - ``TimersScopes`` is also available under the legacy name ``TimerScopes``. - .. py:data:: globus_sdk.scopes.data.TransferScopes diff --git a/docs/services/index.rst b/docs/services/index.rst index 3416a7e79..267c340fb 100644 --- a/docs/services/index.rst +++ b/docs/services/index.rst @@ -45,6 +45,6 @@ very simply: flows groups search - timer + timers transfer gcs diff --git a/docs/services/timer.rst b/docs/services/timers.rst similarity index 85% rename from docs/services/timer.rst rename to docs/services/timers.rst index 8c2b3c8f4..d5d9f082a 100644 --- a/docs/services/timer.rst +++ b/docs/services/timers.rst @@ -3,10 +3,6 @@ Globus Timers .. currentmodule:: globus_sdk -.. note:: - - ``TimersClient`` is also available under a legacy alias, ``TimerClient``. - .. autoclass:: TimersClient :members: :member-order: bysource @@ -50,10 +46,6 @@ Client Errors When an error occurs on calls to the Timers service, a :class:`TimersClient` will raise a ``TimersAPIError``. -.. note:: - - ``TimersAPIError`` is also available under a legacy alias, ``TimerAPIError``. - .. autoclass:: TimersAPIError :members: :show-inheritance: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 042eef493..35311e5d8 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -45,6 +45,15 @@ Then, code can dispatch with From 3.x to 4.0 --------------- +Deprecated Timers Aliases Removed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +During the version 3 lifecycle, the ``TimersClient`` and ``TimersAPIError`` +classes were renamed. Their original names, ``TimerClient`` and +``TimerAPIError`` were retained as compatibility aliases. + +These have been removed. Use ``TimersClient`` and ``TimersAPIError``. + Deprecated Experimental Aliases Removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/scripts/ensure_exports_are_documented.py b/scripts/ensure_exports_are_documented.py index 095d77f3e..f871211c3 100755 --- a/scripts/ensure_exports_are_documented.py +++ b/scripts/ensure_exports_are_documented.py @@ -26,9 +26,6 @@ DEPRECATED_NAMES = { "ComputeFunctionDocument", "ComputeFunctionMetadata", - "TimerAPIError", - "TimerClient", - "TimerScopes", } diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index 61fd2ca7a..5eea2f6b6 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -109,7 +109,6 @@ from .services.search import ( SearchQueryV1, SearchScrollQuery, ) -from .services.timer import TimerAPIError, TimerClient from .services.timers import ( OnceTimerSchedule, RecurringTimerSchedule, @@ -233,8 +232,6 @@ __all__ = ( "SearchQuery", "SearchQueryV1", "SearchScrollQuery", - "TimerAPIError", - "TimerClient", "OnceTimerSchedule", "RecurringTimerSchedule", "TimerJob", diff --git a/src/globus_sdk/scopes/__init__.py b/src/globus_sdk/scopes/__init__.py index 78f9368b2..43401d529 100644 --- a/src/globus_sdk/scopes/__init__.py +++ b/src/globus_sdk/scopes/__init__.py @@ -1,6 +1,3 @@ -import sys -import typing as t - from ._normalize import scopes_to_scope_list, scopes_to_str from .builder import ScopeBuilder from .data import ( @@ -33,25 +30,8 @@ "GroupsScopes", "NexusScopes", "SearchScopes", - "TimerScopes", "TimersScopes", "TransferScopes", "scopes_to_str", "scopes_to_scope_list", ) - - -if t.TYPE_CHECKING: - TimerScopes = TimersScopes -else: - - def __getattr__(name: str) -> t.Any: - from globus_sdk.exc import warn_deprecated - - if name == "TimerScopes": - warn_deprecated( - "'TimerScopes' is a deprecated name. Use 'TimersScopes' instead." - ) - setattr(sys.modules[__name__], name, TimersScopes) - return TimersScopes - raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/src/globus_sdk/services/timer.py b/src/globus_sdk/services/timer.py deleted file mode 100644 index 9fc85567d..000000000 --- a/src/globus_sdk/services/timer.py +++ /dev/null @@ -1,41 +0,0 @@ -from __future__ import annotations - -import sys -import typing as t - -from .timers import TimersAPIError, TimersClient - -__all__ = ( - "TimerAPIError", - "TimerClient", -) - -# legacy aliases -# (when accessed, these will emit deprecation warnings in a future release) -if t.TYPE_CHECKING: - TimerClient = TimersClient - TimerAPIError = TimersAPIError -else: - _RENAMES: dict[str, tuple[str, type]] = { - "TimerClient": ("TimersClient", TimersClient), - "TimerAPIError": ("TimersAPIError", TimersAPIError), - } - - def __getattr__(name: str) -> t.Any: - # In the future, add the following snippet or similar to emit - # deprecation warnings: - # - # from globus_sdk.exc import warn_deprecated - # - # if name in _RENAMES: - # new_name = _RENAMES[name][0] - # warn_deprecated( - # f"'globus_sdk.{name}' has been renamed to 'globus_sdk.{new_name}'. " - # "'{name}' is supported as an alias for now." - # ) - if name in _RENAMES: - value = _RENAMES[name][1] - setattr(sys.modules[__name__], name, value) - return value - - raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/tests/non-pytest/mypy-ignore-tests/timer_client_aliasing.py b/tests/non-pytest/mypy-ignore-tests/timer_client_aliasing.py deleted file mode 100644 index bfef9b6a0..000000000 --- a/tests/non-pytest/mypy-ignore-tests/timer_client_aliasing.py +++ /dev/null @@ -1,12 +0,0 @@ -import globus_sdk - -c = globus_sdk.TimersClient() -c_legacy = globus_sdk.TimerClient() - -# both can call create_timer() -c.create_timer({"foo": "bar"}) -c_legacy.create_timer({"foo": "bar"}) - -# both reject create_timer() -c.create_timer(object()) # type: ignore[arg-type] -c_legacy.create_timer(object()) # type: ignore[arg-type] diff --git a/tests/unit/services/timers/test_legacy_names.py b/tests/unit/services/timers/test_legacy_names.py deleted file mode 100644 index f2e4007f1..000000000 --- a/tests/unit/services/timers/test_legacy_names.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Test that Timers objects are available under their legacy names -(via supported paths only) and that they emit deprecation warnings -as appropriate. -""" - -import pytest - -import globus_sdk -import globus_sdk.scopes - - -def test_importing_legacy_scope_name_works_but_warns(): - # first, remove the object from the module's `__dict__` if it was there - # ensures that access will run `__getattr__` - if "TimerScopes" in globus_sdk.scopes.__dict__: - del globus_sdk.scopes.__dict__["TimerScopes"] - - with pytest.warns( - globus_sdk.exc.RemovedInV4Warning, match="'TimerScopes' is a deprecated name" - ): - from globus_sdk.scopes import TimerScopes - assert TimerScopes is globus_sdk.scopes.TimersScopes - assert hasattr(globus_sdk.scopes, "TimerScopes") - - -def test_bad_access_to_legacy_name_errors(): - # sanity check that by adding module-level 'getattr' we haven't broken the - # AttributeError behaviors of the module - with pytest.raises( - ImportError, match="cannot import name 'FOO' from 'globus_sdk.scopes'" - ): - from globus_sdk.scopes import FOO # noqa: F401 - - with pytest.raises(AttributeError, match="globus_sdk.scopes has no attribute BAR"): - globus_sdk.scopes.BAR - - -def test_importing_legacy_error_name_works(): - # first, remove the object from the module's `__dict__` if it was there - # ensures that access will run `__getattr__` - if "TimerAPIError" in globus_sdk.__dict__: - del globus_sdk.__dict__["TimerAPIError"] - - from globus_sdk import TimerAPIError, TimersAPIError - - assert TimerAPIError is TimersAPIError - assert hasattr(globus_sdk, "TimerAPIError") - - -def test_importing_legacy_client_name_works(): - # first, remove the object from the module's `__dict__` if it was there - # ensures that access will run `__getattr__` - if "TimerClient" in globus_sdk.__dict__: - del globus_sdk.__dict__["TimerClient"] - - from globus_sdk import TimerClient, TimersClient - - assert TimerClient is TimersClient - assert hasattr(globus_sdk, "TimerClient") From b9d3ef9d080c786522ea1bc469231cf5ffab29ca Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 4 Jun 2025 16:38:09 -0500 Subject: [PATCH 018/176] Typo-fix from merge: double-set value 'activity_notification_policy' was being set twice. This was an accidental merge artifact, fixed here. --- src/globus_sdk/services/gcs/data/collection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index 77322124f..f81732782 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -488,7 +488,6 @@ def __init__( # additional fields additional_fields=additional_fields, ) - self._set_value("activity_notification_policy", activity_notification_policy) self["mapped_collection_id"] = mapped_collection_id self["user_credential_id"] = user_credential_id From c2b9883b52aeedff49e80cc09e20cc0e12df7bd8 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 5 Jun 2025 09:20:05 -0500 Subject: [PATCH 019/176] Remove experimental 'scope_parser' alias --- ...n_remove_deprecated_scope_parser_alias.rst | 5 +++ docs/upgrading.rst | 1 + src/globus_sdk/experimental/scope_parser.py | 37 ------------------- .../performance/parser_benchmark.py | 33 +++-------------- .../unit/experimental/test_legacy_support.py | 9 ----- 5 files changed, 11 insertions(+), 74 deletions(-) create mode 100644 changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst delete mode 100644 src/globus_sdk/experimental/scope_parser.py diff --git a/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst b/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst new file mode 100644 index 000000000..97b06497c --- /dev/null +++ b/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst @@ -0,0 +1,5 @@ +Removed +~~~~~~~ + +- ``globus_sdk.experimental.scope_parser`` has been removed. Use + ``globus_sdk.scopes`` instead. (:pr:`NUMBER`) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 35311e5d8..b555a9d12 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -69,6 +69,7 @@ The removed alias and new module names are shown in the table below. :header: "Removed alias", "New name" "``globus_sdk.experimental.auth_requirements_error``", "``globus_sdk.gare``" + "``globus_sdk.experimental.scope_parser``", "``globus_sdk.scopes``" ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/globus_sdk/experimental/scope_parser.py b/src/globus_sdk/experimental/scope_parser.py deleted file mode 100644 index fc62228e7..000000000 --- a/src/globus_sdk/experimental/scope_parser.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Scope parsing has been moved out of experimental into the main SDK. -This module will be removed in a future release but is maintained in the interim for - backwards compatibility. -""" - -from __future__ import annotations - -import sys -import typing as t - -__all__ = ( - "Scope", - "ScopeParseError", - "ScopeCycleError", -) - -if t.TYPE_CHECKING: - from globus_sdk.scopes import Scope, ScopeCycleError, ScopeParseError -else: - - def __getattr__(name: str) -> t.Any: - import globus_sdk.scopes as scopes_module - from globus_sdk.exc import warn_deprecated - - warn_deprecated( - "'globus_sdk.experimental.scope_parser' has been merged into " - "'globus_sdk.scopes'. " - f"Importing '{name}' from `globus_sdk.experimental` is deprecated. " - f"Use `globus_sdk.scopes.{name}` instead." - ) - - value = getattr(scopes_module, name, None) - if value is None: - raise AttributeError(f"module {__name__} has no attribute {name}") - setattr(sys.modules[__name__], name, value) - return value diff --git a/tests/non-pytest/performance/parser_benchmark.py b/tests/non-pytest/performance/parser_benchmark.py index 4ee543434..3a8e685e7 100644 --- a/tests/non-pytest/performance/parser_benchmark.py +++ b/tests/non-pytest/performance/parser_benchmark.py @@ -1,10 +1,5 @@ -from __future__ import annotations - -import sys import timeit -from globus_sdk.scopes._parser import parse_scope_graph - def timeit_test() -> None: for size, num_iterations, style in ( @@ -20,7 +15,7 @@ def timeit_test() -> None: ): if style == "deep": setup = f"""\ -from globus_sdk.experimental.scope_parser import Scope +from globus_sdk.scopes import Scope big_scope = "" for i in range({size}): big_scope += f"foo{{i}}[" @@ -30,7 +25,7 @@ def timeit_test() -> None: """ elif style == "wide": setup = f"""\ -from globus_sdk.experimental.scope_parser import Scope +from globus_sdk.scopes import Scope big_scope = "" for i in range({size}): big_scope += f"foo{{i}} " @@ -67,27 +62,9 @@ def _stats(timing_data: list[float]) -> tuple[float, float, float, float]: return best, worst, average, variance -def parse_test(scope_string: str) -> None: - parsed_graph = parse_scope_graph(scope_string) - print( - "top level scopes:", - ", ".join([name for name, _optional in parsed_graph.top_level_scopes]), - ) - print(parsed_graph) - - def main() -> None: - if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"): - print("This script supports two usage patterns:") - print(" python -m globus_sdk.experimental.scope_parser SCOPE_STRING") - print(" python -m globus_sdk.experimental.scope_parser --timeit") - sys.exit(0) - - print() - if sys.argv[1] == "--timeit": - timeit_test() - else: - parse_test(sys.argv[1]) + timeit_test() -main() +if __name__ == "__main__": + main() diff --git a/tests/unit/experimental/test_legacy_support.py b/tests/unit/experimental/test_legacy_support.py index 82b49b7f0..16f9825fe 100644 --- a/tests/unit/experimental/test_legacy_support.py +++ b/tests/unit/experimental/test_legacy_support.py @@ -14,15 +14,6 @@ from globus_sdk import RemovedInV4Warning -def test_scope_importable_from_experimental(): - with pytest.warns(RemovedInV4Warning): - from globus_sdk.experimental.scope_parser import ( # noqa: F401 - Scope, - ScopeCycleError, - ScopeParseError, - ) - - def test_login_flow_manager_importable_from_experimental(): with pytest.warns(RemovedInV4Warning): from globus_sdk.experimental.login_flow_manager import ( # noqa: F401 From 4d2e6b3117b7edbf03b920c98930075e4be95dc0 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 09:50:51 -0500 Subject: [PATCH 020/176] Updated GCS client to use MISSING defaults --- src/globus_sdk/services/gcs/client.py | 119 ++++++--------- src/globus_sdk/services/gcs/data/role.py | 10 +- .../services/gcs/data/storage_gateway.py | 144 ++++++++---------- .../services/gcs/data/user_credential.py | 17 +-- src/globus_sdk/utils.py | 8 +- .../services/gcs/test_get_collection_list.py | 5 +- .../services/gcs/test_storage_gateways.py | 9 +- 7 files changed, 141 insertions(+), 171 deletions(-) diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 1b767358d..0a15366bb 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -8,6 +8,7 @@ from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import Scope +from globus_sdk.utils import MISSING, MissingType from .connector_table import ConnectorTable from .data import ( @@ -234,8 +235,8 @@ def update_endpoint( endpoint_data: dict[str, t.Any] | EndpointDocument, *, include: ( - t.Iterable[t.Literal["endpoint"]] | t.Literal["endpoint"] | None - ) = None, + t.Iterable[t.Literal["endpoint"]] | t.Literal["endpoint"] | MissingType + ) = MISSING, query_params: dict[str, t.Any] | None = None, ) -> UnpackingGCSResponse: """ @@ -256,10 +257,7 @@ def update_endpoint( :ref: openapi_Endpoint/#patchEndpoint :service: gcs """ - query_params = query_params or {} - if include is not None: - query_params["include"] = utils.commajoin(include) - + query_params = {"include": utils.commajoin(include), **(query_params or {})} return UnpackingGCSResponse( self.patch( "/endpoint", @@ -280,13 +278,13 @@ def update_endpoint( def get_collection_list( self, *, - mapped_collection_id: UUIDLike | None = None, + mapped_collection_id: UUIDLike | MissingType = MISSING, filter: ( # pylint: disable=redefined-builtin - str | t.Iterable[str] | None - ) = None, - include: str | t.Iterable[str] | None = None, - page_size: int | None = None, - marker: str | None = None, + str | t.Iterable[str] | MissingType + ) = MISSING, + include: str | t.Iterable[str] | MissingType = MISSING, + page_size: int | MissingType = MISSING, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableGCSResponse: """ @@ -313,20 +311,14 @@ def get_collection_list( :ref: openapi_Collections/#ListCollections :service: gcs """ - if query_params is None: - query_params = {} - if include is not None: - query_params["include"] = ",".join(utils.safe_strseq_iter(include)) - if page_size is not None: - query_params["page_size"] = page_size - if marker is not None: - query_params["marker"] = marker - if mapped_collection_id is not None: - query_params["mapped_collection_id"] = mapped_collection_id - if filter is not None: - if isinstance(filter, str): - filter = [filter] - query_params["filter"] = ",".join(filter) + query_params = { + "include": utils.commajoin(include), + "page_size": page_size, + "marker": marker, + "mapped_collection_id": mapped_collection_id, + "filter": utils.commajoin(filter), + **(query_params or {}), + } return IterableGCSResponse(self.get("collections", query_params=query_params)) def get_collection( @@ -455,9 +447,9 @@ def delete_collection( def get_storage_gateway_list( self, *, - include: None | str | t.Iterable[str] = None, - page_size: int | None = None, - marker: str | None = None, + include: str | t.Iterable[str] | MissingType = MISSING, + page_size: int | MissingType = MISSING, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableGCSResponse: """ @@ -486,14 +478,12 @@ def get_storage_gateway_list( :ref: openapi_Storage_Gateways/#getStorageGateways :service: gcs """ - if query_params is None: - query_params = {} - if include is not None: - query_params["include"] = ",".join(utils.safe_strseq_iter(include)) - if page_size is not None: - query_params["page_size"] = page_size - if marker is not None: - query_params["marker"] = marker + query_params = { + "include": utils.commajoin(include), + "page_size": page_size, + "marker": marker, + **(query_params or {}), + } return IterableGCSResponse( self.get("/storage_gateways", query_params=query_params) ) @@ -530,7 +520,7 @@ def get_storage_gateway( self, storage_gateway_id: UUIDLike, *, - include: None | str | t.Iterable[str] = None, + include: str | t.Iterable[str] | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> UnpackingGCSResponse: """ @@ -553,11 +543,7 @@ def get_storage_gateway( :ref: openapi_Storage_Gateways/#getStorageGateway :service: gcs """ - if query_params is None: - query_params = {} - if include is not None: - query_params["include"] = ",".join(utils.safe_strseq_iter(include)) - + query_params = {"include": utils.commajoin(include), **(query_params or {})} return UnpackingGCSResponse( self.get( f"/storage_gateways/{storage_gateway_id}", @@ -633,10 +619,10 @@ def delete_storage_gateway( ) def get_role_list( self, - collection_id: UUIDLike | None = None, - include: str | None = None, - page_size: int | None = None, - marker: str | None = None, + collection_id: UUIDLike | MissingType = MISSING, + include: str | MissingType = MISSING, + page_size: int | MissingType = MISSING, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableGCSResponse: """ @@ -663,17 +649,13 @@ def get_role_list( :ref: openapi_Roles/#listRoles :service: gcs """ - if query_params is None: - query_params = {} - if include is not None: - query_params["include"] = include - if page_size is not None: - query_params["page_size"] = page_size - if marker is not None: - query_params["marker"] = marker - if collection_id is not None: - query_params["collection_id"] = collection_id - + query_params = { + "include": include, + "page_size": page_size, + "marker": marker, + "collection_id": collection_id, + **(query_params or {}), + } path = "/roles" return IterableGCSResponse(self.get(path, query_params=query_params)) @@ -759,10 +741,10 @@ def delete_role( ) def get_user_credential_list( self, - storage_gateway: UUIDLike | None = None, + storage_gateway: UUIDLike | MissingType = MISSING, + page_size: int | MissingType = MISSING, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, - page_size: int | None = None, - marker: str | None = None, ) -> IterableGCSResponse: """ List User Credentials @@ -783,15 +765,12 @@ def get_user_credential_list( :ref: openapi_User_Credentials/#getUserCredentials :service: gcs """ - if query_params is None: - query_params = {} - if storage_gateway is not None: - query_params["storage_gateway"] = storage_gateway - if page_size is not None: - query_params["page_size"] = page_size - if marker is not None: - query_params["marker"] = marker - + query_params = { + "storage_gateway": storage_gateway, + "page_size": page_size, + "marker": marker, + **(query_params or {}), + } path = "/user_credentials" return IterableGCSResponse(self.get(path, query_params=query_params)) diff --git a/src/globus_sdk/services/gcs/data/role.py b/src/globus_sdk/services/gcs/data/role.py index fc21307e6..a947a5967 100644 --- a/src/globus_sdk/services/gcs/data/role.py +++ b/src/globus_sdk/services/gcs/data/role.py @@ -4,6 +4,7 @@ from globus_sdk import utils from globus_sdk._types import UUIDLike +from globus_sdk.utils import MISSING, MissingType class GCSRoleDocument(utils.PayloadWrapper): @@ -24,9 +25,9 @@ class GCSRoleDocument(utils.PayloadWrapper): def __init__( self, DATA_TYPE: str = "role#1.0.0", - collection: UUIDLike | None = None, - principal: str | None = None, - role: str | None = None, + collection: UUIDLike | MissingType = MISSING, + principal: str | MissingType = MISSING, + role: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -36,5 +37,4 @@ def __init__( principal=principal, role=role, ) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index 4d27a121f..8f0ff02bf 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -5,6 +5,7 @@ from globus_sdk import utils from globus_sdk._types import UUIDLike +from globus_sdk.utils import MISSING, MissingType from ._common import DatatypeCallback, ensure_datatype @@ -56,18 +57,18 @@ class StorageGatewayDocument(utils.PayloadWrapper): def __init__( self, - DATA_TYPE: str | None = None, - display_name: str | None = None, - connector_id: UUIDLike | None = None, - root: str | None = None, - identity_mappings: None | t.Iterable[dict[str, t.Any]] = None, - policies: StorageGatewayPolicies | dict[str, t.Any] | None = None, - allowed_domains: t.Iterable[str] | None = None, - high_assurance: bool | None = None, - require_mfa: bool | None = None, - authentication_timeout_mins: int | None = None, - users_allow: t.Iterable[str] | None = None, - users_deny: t.Iterable[str] | None = None, + DATA_TYPE: str | MissingType = MISSING, + display_name: str | MissingType = MISSING, + connector_id: UUIDLike | MissingType = MISSING, + root: str | MissingType = MISSING, + identity_mappings: t.Iterable[dict[str, t.Any]] | MissingType = MISSING, + policies: StorageGatewayPolicies | dict[str, t.Any] | MissingType = MISSING, + allowed_domains: t.Iterable[str] | MissingType = MISSING, + high_assurance: bool | MissingType = MISSING, + require_mfa: bool | MissingType = MISSING, + authentication_timeout_mins: int | MissingType = MISSING, + users_allow: t.Iterable[str] | MissingType = MISSING, + users_deny: t.Iterable[str] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -86,8 +87,7 @@ def __init__( self._set_optints(authentication_timeout_mins=authentication_timeout_mins) self._set_value("identity_mappings", identity_mappings, callback=list) self._set_value("policies", policies) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) ensure_datatype(self) @@ -118,15 +118,14 @@ class POSIXStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "posix_storage_policies#1.0.0", - groups_allow: t.Iterable[str] | None = None, - groups_deny: t.Iterable[str] | None = None, + groups_allow: t.Iterable[str] | MissingType = MISSING, + groups_deny: t.Iterable[str] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() self._set_optstrs(DATA_TYPE=DATA_TYPE) self._set_optstrlists(groups_allow=groups_allow, groups_deny=groups_deny) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class POSIXStagingStoragePolicies(StorageGatewayPolicies): @@ -149,10 +148,10 @@ class POSIXStagingStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "posix_staging_storage_policies#1.0.0", - groups_allow: t.Iterable[str] | None = None, - groups_deny: t.Iterable[str] | None = None, - stage_app: str | None = None, - environment: t.Iterable[dict[str, str]] | None = None, + groups_allow: t.Iterable[str] | MissingType = MISSING, + groups_deny: t.Iterable[str] | MissingType = MISSING, + stage_app: str | MissingType = MISSING, + environment: t.Iterable[dict[str, str]] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -163,8 +162,7 @@ def __init__( environment, callback=lambda env_iter: [{**e} for e in env_iter], ) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class BlackPearlStoragePolicies(StorageGatewayPolicies): @@ -185,8 +183,8 @@ class BlackPearlStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "blackpearl_storage_policies#1.0.0", - s3_endpoint: str | None = None, - bp_access_id_file: str | None = None, + s3_endpoint: str | MissingType = MISSING, + bp_access_id_file: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -195,8 +193,7 @@ def __init__( s3_endpoint=s3_endpoint, bp_access_id_file=bp_access_id_file, ) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class BoxStoragePolicies(StorageGatewayPolicies): @@ -216,15 +213,14 @@ class BoxStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "box_storage_policies#1.0.0", - enterpriseID: str | None = None, - boxAppSettings: dict[str, t.Any] | None = None, + enterpriseID: str | MissingType = MISSING, + boxAppSettings: dict[str, t.Any] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() self._set_optstrs(DATA_TYPE=DATA_TYPE, enterpriseID=enterpriseID) self._set_value("boxAppSettings", boxAppSettings) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class CephStoragePolicies(StorageGatewayPolicies): @@ -247,10 +243,10 @@ class CephStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "ceph_storage_policies#1.0.0", - s3_endpoint: str | None = None, - s3_buckets: t.Iterable[str] | None = None, - ceph_admin_key_id: str | None = None, - ceph_admin_secret_key: str | None = None, + s3_endpoint: str | MissingType = MISSING, + s3_buckets: t.Iterable[str] | MissingType = MISSING, + ceph_admin_key_id: str | MissingType = MISSING, + ceph_admin_secret_key: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -261,8 +257,7 @@ def __init__( ceph_admin_secret_key=ceph_admin_secret_key, ) self._set_optstrlists(s3_buckets=s3_buckets) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class GoogleDriveStoragePolicies(StorageGatewayPolicies): @@ -283,16 +278,15 @@ class GoogleDriveStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "google_drive_storage_policies#1.0.0", - client_id: str | None = None, - secret: str | None = None, - user_api_rate_quota: int | None = None, + client_id: str | MissingType = MISSING, + secret: str | MissingType = MISSING, + user_api_rate_quota: int | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() self._set_optstrs(DATA_TYPE=DATA_TYPE, client_id=client_id, secret=secret) self._set_optints(user_api_rate_quota=user_api_rate_quota) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class GoogleCloudStoragePolicies(StorageGatewayPolicies): @@ -323,19 +317,18 @@ class GoogleCloudStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "google_cloud_storage_policies#1.0.0", - client_id: str | None = None, - secret: str | None = None, - service_account_key: dict[str, t.Any] | None = None, - buckets: t.Iterable[str] | None = None, - projects: t.Iterable[str] | None = None, + client_id: str | MissingType = MISSING, + secret: str | MissingType = MISSING, + service_account_key: dict[str, t.Any] | MissingType = MISSING, + buckets: t.Iterable[str] | MissingType = MISSING, + projects: t.Iterable[str] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() self._set_optstrs(DATA_TYPE=DATA_TYPE, client_id=client_id, secret=secret) self._set_optstrlists(buckets=buckets, projects=projects) self._set_value("service_account_key", service_account_key) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class OneDriveStoragePolicies(StorageGatewayPolicies): @@ -357,10 +350,10 @@ class OneDriveStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "onedrive_storage_policies#1.0.0", - client_id: str | None = None, - secret: str | None = None, - tenant: str | None = None, - user_api_rate_limit: int | None = None, + client_id: str | MissingType = MISSING, + secret: str | MissingType = MISSING, + tenant: str | MissingType = MISSING, + user_api_rate_limit: int | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -368,8 +361,7 @@ def __init__( DATA_TYPE=DATA_TYPE, client_id=client_id, secret=secret, tenant=tenant ) self._set_optints(user_api_rate_limit=user_api_rate_limit) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class AzureBlobStoragePolicies(StorageGatewayPolicies): @@ -393,12 +385,12 @@ class AzureBlobStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "azure_blob_storage_policies#1.0.0", - client_id: str | None = None, - secret: str | None = None, - tenant: str | None = None, - account: str | None = None, - auth_type: str | None = None, - adls: bool | None = None, + client_id: str | MissingType = MISSING, + secret: str | MissingType = MISSING, + tenant: str | MissingType = MISSING, + account: str | MissingType = MISSING, + auth_type: str | MissingType = MISSING, + adls: bool | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -411,8 +403,7 @@ def __init__( auth_type=auth_type, ) self._set_optbools(adls=adls) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class S3StoragePolicies(StorageGatewayPolicies): @@ -435,17 +426,16 @@ class S3StoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "s3_storage_policies#1.0.0", - s3_endpoint: str | None = None, - s3_buckets: t.Iterable[str] | None = None, - s3_user_credential_required: bool | None = None, + s3_endpoint: str | MissingType = MISSING, + s3_buckets: t.Iterable[str] | MissingType = MISSING, + s3_user_credential_required: bool | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() self._set_optstrs(DATA_TYPE=DATA_TYPE, s3_endpoint=s3_endpoint) self._set_optbools(s3_user_credential_required=s3_user_credential_required) self._set_optstrlists(s3_buckets=s3_buckets) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class ActiveScaleStoragePolicies(S3StoragePolicies): @@ -470,8 +460,8 @@ class IrodsStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "irods_storage_policies#1.0.0", - irods_environment_file: str | None = None, - irods_authentication_file: str | None = None, + irods_environment_file: str | MissingType = MISSING, + irods_authentication_file: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -480,8 +470,7 @@ def __init__( irods_environment_file=irods_environment_file, irods_authentication_file=irods_authentication_file, ) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) class HPSSStoragePolicies(StorageGatewayPolicies): @@ -501,9 +490,9 @@ class HPSSStoragePolicies(StorageGatewayPolicies): def __init__( self, DATA_TYPE: str = "hpss_storage_policies#1.0.0", - authentication_mech: str | None = None, - authenticator: str | None = None, - uda_checksum_support: bool | None = None, + authentication_mech: str | MissingType = MISSING, + authenticator: str | MissingType = MISSING, + uda_checksum_support: bool | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -513,5 +502,4 @@ def __init__( authenticator=authenticator, ) self._set_optbools(uda_checksum_support=uda_checksum_support) - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) diff --git a/src/globus_sdk/services/gcs/data/user_credential.py b/src/globus_sdk/services/gcs/data/user_credential.py index 41bae8b64..4f2fefe36 100644 --- a/src/globus_sdk/services/gcs/data/user_credential.py +++ b/src/globus_sdk/services/gcs/data/user_credential.py @@ -4,6 +4,7 @@ from globus_sdk import utils from globus_sdk._types import UUIDLike +from globus_sdk.utils import MISSING, MissingType class UserCredentialDocument(utils.PayloadWrapper): @@ -27,12 +28,12 @@ class UserCredentialDocument(utils.PayloadWrapper): def __init__( self, DATA_TYPE: str = "user_credential#1.0.0", - identity_id: UUIDLike | None = None, - connector_id: UUIDLike | None = None, - username: str | None = None, - display_name: str | None = None, - storage_gateway_id: UUIDLike | None = None, - policies: dict[str, t.Any] | None = None, + identity_id: UUIDLike | MissingType = MISSING, + connector_id: UUIDLike | MissingType = MISSING, + username: str | MissingType = MISSING, + display_name: str | MissingType = MISSING, + storage_gateway_id: UUIDLike | MissingType = MISSING, + policies: dict[str, t.Any] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() @@ -45,6 +46,4 @@ def __init__( storage_gateway_id=storage_gateway_id, ) self._set_value("policies", policies) - - if additional_fields is not None: - self.update(additional_fields) + self.update(additional_fields or {}) diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index 08a1a214a..09f1e6c9f 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -183,7 +183,7 @@ def _set_value( :param callback: An optional callback to apply to the value immediately before it is set. """ - if val is not None: + if val is not None and val is not MISSING: self[key] = callback(val) if callback else val def _set_optstrs(self, **kwargs: t.Any) -> None: @@ -195,7 +195,9 @@ def _set_optstrs(self, **kwargs: t.Any) -> None: for k, v in kwargs.items(): self._set_value(k, v, callback=str) - def _set_optstrlists(self, **kwargs: t.Iterable[t.Any] | None) -> None: + def _set_optstrlists( + self, **kwargs: t.Iterable[t.Any] | None | MissingType + ) -> None: """ Convenience function for setting a collection of omittable string list values. @@ -204,7 +206,7 @@ def _set_optstrlists(self, **kwargs: t.Iterable[t.Any] | None) -> None: for k, v in kwargs.items(): self._set_value(k, v, callback=lambda x: list(safe_strseq_iter(x))) - def _set_optbools(self, **kwargs: bool | None) -> None: + def _set_optbools(self, **kwargs: bool | None | MissingType) -> None: """ Convenience function for setting a collection of omittable bool values. diff --git a/tests/functional/services/gcs/test_get_collection_list.py b/tests/functional/services/gcs/test_get_collection_list.py index 12382aae4..b13aa0358 100644 --- a/tests/functional/services/gcs/test_get_collection_list.py +++ b/tests/functional/services/gcs/test_get_collection_list.py @@ -2,6 +2,7 @@ from globus_sdk import GCSAPIError from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING def test_get_collection_list(client): @@ -22,7 +23,7 @@ def test_get_collection_list(client): @pytest.mark.parametrize( "include_param, expected", ( - (None, None), + (MISSING, None), ("foo", "foo"), ("foo,bar", "foo,bar"), (("foo", "bar"), "foo,bar"), @@ -32,7 +33,7 @@ def test_get_collection_list_include_param(client, include_param, expected): load_response(client.get_collection_list) client.get_collection_list(include=include_param) req = get_last_request() - if include_param is not None: + if include_param is not MISSING: assert "include" in req.params assert req.params["include"] == expected else: diff --git a/tests/functional/services/gcs/test_storage_gateways.py b/tests/functional/services/gcs/test_storage_gateways.py index 83e3328e5..16fcb8d7c 100644 --- a/tests/functional/services/gcs/test_storage_gateways.py +++ b/tests/functional/services/gcs/test_storage_gateways.py @@ -4,11 +4,12 @@ import globus_sdk from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING @pytest.mark.parametrize( "include_param", - [None, "private_policies", "private_policies,foo", ("private_policies", "foo")], + [MISSING, "private_policies", "private_policies,foo", ("private_policies", "foo")], ) def test_get_storage_gateway_list(client, include_param): meta = load_response(client.get_storage_gateway_list).metadata @@ -28,7 +29,7 @@ def test_get_storage_gateway_list(client, include_param): req = get_last_request() assert req.body is None parsed_qs = urllib.parse.parse_qs(urllib.parse.urlparse(req.url).query) - if include_param is None: + if include_param is MISSING: assert parsed_qs == {} elif isinstance(include_param, str): assert parsed_qs == {"include": [include_param]} @@ -65,7 +66,7 @@ def test_create_storage_gateway_validation_error(client): @pytest.mark.parametrize( "include_param", - [None, "private_policies", "private_policies,foo", ("private_policies", "foo")], + [MISSING, "private_policies", "private_policies,foo", ("private_policies", "foo")], ) def test_get_storage_gateway(client, include_param): meta = load_response(client.get_storage_gateway).metadata @@ -80,7 +81,7 @@ def test_get_storage_gateway(client, include_param): req = get_last_request() assert req.body is None parsed_qs = urllib.parse.parse_qs(urllib.parse.urlparse(req.url).query) - if include_param is None: + if include_param is MISSING: assert parsed_qs == {} elif isinstance(include_param, str): assert parsed_qs == {"include": [include_param]} From 6af3c45884a375d96c2a70527a543881ab2fa7e1 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 09:53:11 -0500 Subject: [PATCH 021/176] Added changelog --- ...250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst diff --git a/changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst b/changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst new file mode 100644 index 000000000..5695e1683 --- /dev/null +++ b/changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst @@ -0,0 +1,4 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the GCS client. (:pr:`1214`) \ No newline at end of file From 97053e8e205e3287b3eabc6802b07e6582f6ac5c Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 09:53:51 -0500 Subject: [PATCH 022/176] Update search client classes to use MISSING defaults (#1207) * Updated search client to use MISSING defaults * Added changelog * Fix: mypy * Requested change: changelog markdown * Requested change: format histogram helper * Requested changes: query param expansions --- ...uecke_sc_15807_search_missing_defaults.rst | 4 + src/globus_sdk/services/search/client.py | 90 +++++++-------- src/globus_sdk/services/search/data.py | 107 +++++++++--------- src/globus_sdk/utils.py | 8 ++ .../functional/services/search/test_search.py | 12 +- tests/unit/helpers/test_search.py | 15 +-- 6 files changed, 119 insertions(+), 117 deletions(-) create mode 100644 changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst diff --git a/changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst b/changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst new file mode 100644 index 000000000..f34cabb4d --- /dev/null +++ b/changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst @@ -0,0 +1,4 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Search client. (:pr:`1207`) \ No newline at end of file diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index 20b159bba..bb5319ad8 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -7,6 +7,7 @@ from globus_sdk._types import UUIDLike from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import Scope, SearchScopes +from globus_sdk.utils import MISSING, MissingType from .data import SearchQuery, SearchScrollQuery from .errors import SearchAPIError @@ -234,9 +235,9 @@ def search( index_id: UUIDLike, q: str, *, - offset: int = 0, - limit: int = 10, - advanced: bool = False, + offset: int | MissingType = MISSING, + limit: int | MissingType = MISSING, + advanced: bool | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -278,17 +279,13 @@ def search( .. expandtestfixture:: search.search """ # noqa: E501 - if query_params is None: - query_params = {} - query_params.update( - { - "q": q, - "offset": offset, - "limit": limit, - "advanced": advanced, - } - ) - + query_params = { + "q": q, + "offset": offset, + "limit": limit, + "advanced": advanced, + **(query_params or {}), + } log.debug(f"SearchClient.search({index_id}, ...)") return self.get(f"/v1/index/{index_id}/search", query_params=query_params) @@ -304,8 +301,8 @@ def post_search( index_id: UUIDLike, data: dict[str, t.Any] | SearchQuery, *, - offset: int | None = None, - limit: int | None = None, + offset: int | MissingType = MISSING, + limit: int | MissingType = MISSING, ) -> response.GlobusHTTPResponse: """ Execute a complex Search Query, using a query document to express filters, @@ -360,12 +357,11 @@ def post_search( """ log.debug(f"SearchClient.post_search({index_id}, ...)") add_kwargs = {} - if offset is not None: + if not isinstance(offset, MissingType): add_kwargs["offset"] = offset - if limit is not None: + if not isinstance(limit, MissingType): add_kwargs["limit"] = limit - if add_kwargs: - data = {**data, **add_kwargs} + data = {**data, **add_kwargs} return self.post(f"v1/index/{index_id}/search", data=data) @paging.has_paginator(paging.MarkerPaginator, items_key="gmeta") @@ -374,7 +370,7 @@ def scroll( index_id: UUIDLike, data: dict[str, t.Any] | SearchScrollQuery, *, - marker: str | None = None, + marker: str | MissingType = MISSING, ) -> response.GlobusHTTPResponse: """ Scroll all data in a Search index. The paginated version of this API should @@ -412,10 +408,9 @@ def scroll( """ log.debug(f"SearchClient.scroll({index_id}, ...)") add_kwargs = {} - if marker is not None: + if not isinstance(marker, MissingType): add_kwargs["marker"] = marker - if add_kwargs: - data = {**data, **add_kwargs} + data = {**data, **add_kwargs} return self.post(f"v1/index/{index_id}/scroll", data=data) # @@ -579,9 +574,10 @@ def batch_delete_by_subject( # convert the provided subjects to a list and use the "safe iter" helper to # ensure that a single string is *not* treated as an iterable of strings, # which is usually not intentional - body = {"subjects": list(utils.safe_strseq_iter(subjects))} - if additional_params: - body.update(additional_params) + body = { + "subjects": list(utils.safe_strseq_iter(subjects)), + **(additional_params or {}), + } return self.post(f"/v1/index/{index_id}/batch_delete_by_subject", data=body) # @@ -621,10 +617,11 @@ def get_subject( .. extdoclink:: Get By Subject :ref: search/reference/get_subject/ """ - if query_params is None: - query_params = {} - query_params["subject"] = subject log.debug(f"SearchClient.get_subject({index_id}, {subject}, ...)") + query_params = { + "subject": subject, + **(query_params or {}), + } return self.get(f"/v1/index/{index_id}/subject", query_params=query_params) def delete_subject( @@ -664,11 +661,11 @@ def delete_subject( .. extdoclink:: Delete By Subject :ref: search/reference/delete_subject/ """ - if query_params is None: - query_params = {} - query_params["subject"] = subject - log.debug(f"SearchClient.delete_subject({index_id}, {subject}, ...)") + query_params = { + "subject": subject, + **(query_params or {}), + } return self.delete(f"/v1/index/{index_id}/subject", query_params=query_params) # @@ -680,7 +677,7 @@ def get_entry( index_id: UUIDLike, subject: str, *, - entry_id: str | None = None, + entry_id: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -720,17 +717,16 @@ def get_entry( .. extdoclink:: Get Entry :ref: search/reference/get_entry/ """ # noqa: E501 - if query_params is None: - query_params = {} - query_params["subject"] = subject - if entry_id is not None: - query_params["entry_id"] = entry_id - log.debug( "SearchClient.get_entry({}, {}, {}, ...)".format( index_id, subject, entry_id ) ) + query_params = { + "entry_id": entry_id, + "subject": subject, + **(query_params or {}), + } return self.get(f"/v1/index/{index_id}/entry", query_params=query_params) def create_entry( @@ -849,7 +845,7 @@ def delete_entry( index_id: UUIDLike, subject: str, *, - entry_id: str | None = None, + entry_id: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -889,16 +885,16 @@ def delete_entry( .. extdoclink:: Delete Entry :ref: search/reference/delete_entry/ """ # noqa: E501 - if query_params is None: - query_params = {} - query_params["subject"] = subject - if entry_id is not None: - query_params["entry_id"] = entry_id log.debug( "SearchClient.delete_entry({}, {}, {}, ...)".format( index_id, subject, entry_id ) ) + query_params = { + "entry_id": entry_id, + "subject": subject, + **(query_params or {}), + } return self.delete(f"/v1/index/{index_id}/entry", query_params=query_params) # diff --git a/src/globus_sdk/services/search/data.py b/src/globus_sdk/services/search/data.py index dd593f8d1..dc3020998 100644 --- a/src/globus_sdk/services/search/data.py +++ b/src/globus_sdk/services/search/data.py @@ -3,6 +3,7 @@ import typing as t from globus_sdk import exc, utils +from globus_sdk.utils import MISSING, MissingType # workaround for absence of Self type # for the workaround and some background, see: @@ -10,6 +11,15 @@ SearchQueryT = t.TypeVar("SearchQueryT", bound="SearchQueryBase") +def _format_histogram_range( + value: tuple[t.Any, t.Any] | MissingType, +) -> dict[str, t.Any] | MissingType: + if isinstance(value, MissingType): + return MISSING + low, high = value + return {"low": low, "high": high} + + # an internal class for declaring multiple related types with shared methods class SearchQueryBase(utils.PayloadWrapper): """ @@ -107,25 +117,21 @@ class SearchQuery(SearchQueryBase): def __init__( self, - q: str | None = None, + q: str | MissingType = MISSING, *, - limit: int | None = None, - offset: int | None = None, - advanced: bool | None = None, + limit: int | MissingType = MISSING, + offset: int | MissingType = MISSING, + advanced: bool | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() exc.warn_deprecated("'SearchQuery' is deprecated. Use 'SearchQueryV1' instead.") - if q is not None: - self["q"] = q - if limit is not None: - self["limit"] = limit - if offset is not None: - self["offset"] = offset - if advanced is not None: - self["advanced"] = advanced - if additional_fields is not None: - self.update(additional_fields) + + self["q"] = q + self["limit"] = limit + self["offset"] = offset + self["advanced"] = advanced + self.update(additional_fields or {}) def set_offset(self, offset: int) -> SearchQuery: """ @@ -143,9 +149,9 @@ def add_facet( *, # pylint: disable=redefined-builtin type: str = "terms", - size: int | None = None, - date_interval: str | None = None, - histogram_range: tuple[t.Any, t.Any] | None = None, + size: int | MissingType = MISSING, + date_interval: str | MissingType = MISSING, + histogram_range: tuple[t.Any, t.Any] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> SearchQuery: """ @@ -164,15 +170,11 @@ def add_facet( "name": name, "field_name": field_name, "type": type, + "size": size, + "date_interval": date_interval, + "histogram_range": _format_histogram_range(histogram_range), **(additional_fields or {}), } - if size is not None: - facet["size"] = size - if date_interval is not None: - facet["date_interval"] = date_interval - if histogram_range is not None: - low, high = histogram_range - facet["histogram_range"] = {"low": low, "high": high} self["facets"].append(facet) return self @@ -204,7 +206,7 @@ def add_sort( self, field_name: str, *, - order: str | None = None, + order: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> SearchQuery: """ @@ -215,9 +217,11 @@ def add_sort( :param additional_fields: additional data to include in the sort document """ self["sort"] = self.get("sort", []) - sort = {"field_name": field_name, **(additional_fields or {})} - if order is not None: - sort["order"] = order + sort = { + "field_name": field_name, + "order": order, + **(additional_fields or {}), + } self["sort"].append(sort) return self @@ -245,16 +249,16 @@ class SearchQueryV1(utils.PayloadWrapper): def __init__( self, *, - q: str | utils.MissingType = utils.MISSING, - limit: int | utils.MissingType = utils.MISSING, - offset: int | utils.MissingType = utils.MISSING, - advanced: bool | utils.MissingType = utils.MISSING, - filters: list[dict[str, t.Any]] | utils.MissingType = utils.MISSING, - facets: list[dict[str, t.Any]] | utils.MissingType = utils.MISSING, - post_facet_filters: list[dict[str, t.Any]] | utils.MissingType = utils.MISSING, - boosts: list[dict[str, t.Any]] | utils.MissingType = utils.MISSING, - sort: list[dict[str, t.Any]] | utils.MissingType = utils.MISSING, - additional_fields: dict[str, t.Any] | utils.MissingType = utils.MISSING, + q: str | MissingType = MISSING, + limit: int | MissingType = MISSING, + offset: int | MissingType = MISSING, + advanced: bool | MissingType = MISSING, + filters: list[dict[str, t.Any]] | MissingType = MISSING, + facets: list[dict[str, t.Any]] | MissingType = MISSING, + post_facet_filters: list[dict[str, t.Any]] | MissingType = MISSING, + boosts: list[dict[str, t.Any]] | MissingType = MISSING, + sort: list[dict[str, t.Any]] | MissingType = MISSING, + additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() self["@version"] = "query#1.0.0" @@ -268,9 +272,7 @@ def __init__( self["post_facet_filters"] = post_facet_filters self["boosts"] = boosts self["sort"] = sort - - if not isinstance(additional_fields, utils.MissingType): - self.update(additional_fields) + self.update(additional_fields or {}) class SearchScrollQuery(SearchQueryBase): @@ -295,24 +297,19 @@ class SearchScrollQuery(SearchQueryBase): def __init__( self, - q: str | None = None, + q: str | MissingType = MISSING, *, - limit: int | None = None, - advanced: bool | None = None, - marker: str | None = None, + limit: int | MissingType = MISSING, + advanced: bool | MissingType = MISSING, + marker: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - if q is not None: - self["q"] = q - if limit is not None: - self["limit"] = limit - if advanced is not None: - self["advanced"] = advanced - if marker is not None: - self["marker"] = marker - if additional_fields is not None: - self.update(additional_fields) + self["q"] = q + self["limit"] = limit + self["advanced"] = advanced + self["marker"] = marker + self.update(additional_fields or {}) def set_marker(self, marker: str) -> SearchScrollQuery: """ diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index 08a1a214a..a18bfe03c 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -57,6 +57,14 @@ def __repr__(self) -> str: MISSING = MissingType() +@t.overload +def filter_missing(data: dict[str, t.Any]) -> dict[str, t.Any]: ... + + +@t.overload +def filter_missing(data: None) -> None: ... + + def filter_missing(data: dict[str, t.Any] | None) -> dict[str, t.Any] | None: if data is None: return None diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index 97c17b29d..9bc1aab67 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -7,6 +7,7 @@ import globus_sdk from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import filter_missing from tests.common import register_api_route_fixture_file @@ -31,12 +32,7 @@ def test_search_query_simple(search_client): req = get_last_request() assert req.body is None parsed_qs = urllib.parse.parse_qs(urllib.parse.urlparse(req.url).query) - assert parsed_qs == { - "q": ["foo"], - "advanced": ["False"], - "limit": ["10"], - "offset": ["0"], - } + assert parsed_qs == {"q": ["foo"]} @pytest.mark.parametrize("query_doc", [{"q": "foo"}, {"q": "foo", "limit": 10}]) @@ -73,7 +69,7 @@ def test_search_post_query_with_legacy_helper(search_client): req = get_last_request() assert req.body is not None req_body = json.loads(req.body) - assert req_body == dict(query_doc) + assert req_body == filter_missing(query_doc) def test_search_post_query_simple_with_v1_helper(search_client): @@ -181,4 +177,4 @@ def test_search_paginated_scroll_query(search_client, query_doc): assert data[1]["entries"][0]["content"]["foo"] == "baz" # confirm that pagination was not side-effecting - assert "marker" not in query_doc + assert "marker" not in filter_missing(query_doc) diff --git a/tests/unit/helpers/test_search.py b/tests/unit/helpers/test_search.py index d44a64f56..f37c9178f 100644 --- a/tests/unit/helpers/test_search.py +++ b/tests/unit/helpers/test_search.py @@ -5,6 +5,7 @@ import pytest from globus_sdk import RemovedInV4Warning, SearchQuery, SearchQueryV1, utils +from globus_sdk.utils import filter_missing def test_init_legacy(): @@ -19,7 +20,7 @@ def test_init_legacy_no_args(): with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): query = SearchQuery() - assert len(query) == 0 + assert len(filter_missing(query)) == 0 def test_init_legacy_additional_fields(): @@ -67,7 +68,7 @@ def test_set_method(attrname): query = SearchQuery() method = getattr(query, "set_{}".format("query" if attrname == "q" else attrname)) # start absent - assert attrname not in query + assert attrname not in filter_missing(query) # returns self assert method("foo") is query # sets value @@ -84,7 +85,7 @@ def test_add_facet(): assert query.add_facet("facetname", "fieldname") is query assert query["facets"] assert len(query["facets"]) == 1 - assert query["facets"][0] == { + assert filter_missing(query["facets"][0]) == { "type": "terms", "name": "facetname", "field_name": "fieldname", @@ -93,7 +94,7 @@ def test_add_facet(): # terms with size query.add_facet("n", "f", size=5) assert len(query["facets"]) == 2 - assert query["facets"][1] == { + assert filter_missing(query["facets"][1]) == { "type": "terms", "name": "n", "field_name": "f", @@ -109,7 +110,7 @@ def test_add_facet(): histogram_range=(1870, 1880), ) assert len(query["facets"]) == 3 - assert query["facets"][2] == { + assert filter_missing(query["facets"][2]) == { "type": "date_histogram", "name": "n", "field_name": "f", @@ -122,7 +123,7 @@ def test_add_facet(): "facetname", "fieldname", additional_fields={"nonexistentparam": "value1"} ) assert len(query["facets"]) == 4 - assert query["facets"][3] == { + assert filter_missing(query["facets"][3]) == { "type": "terms", "name": "facetname", "field_name": "fieldname", @@ -203,7 +204,7 @@ def test_add_sort(): assert query["sort"] assert len(query["sort"]) == 1 - assert query["sort"][0] == {"field_name": "f"} + assert filter_missing(query["sort"][0]) == {"field_name": "f"} # with order query.add_sort("f", order="asc") From c328c8ad55a3f4a7e2bbc8bd3df1380ebd1807a2 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 09:57:00 -0500 Subject: [PATCH 023/176] Update compute and groups clients classes to use MISSING defaults (#1212) * Updated compute client to use MISSING defaults * Updated groups client to use MISSING defaults * Added changelog --- ...ax.tuecke_sc_15807_groups_compute_missing_defaults.rst | 5 +++++ src/globus_sdk/services/compute/client.py | 5 +++-- src/globus_sdk/services/groups/client.py | 8 +++----- 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst diff --git a/changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst b/changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst new file mode 100644 index 000000000..35af6ca06 --- /dev/null +++ b/changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst @@ -0,0 +1,5 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Compute client. (:pr:`1212`) +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Groups client. (:pr:`1212`) \ No newline at end of file diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index 6e2625331..027947b6e 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -6,6 +6,7 @@ from globus_sdk import GlobusHTTPResponse, client, utils from globus_sdk._types import UUIDLike from globus_sdk.scopes import ComputeScopes, Scope +from globus_sdk.utils import MISSING, MissingType from .errors import ComputeAPIError @@ -26,7 +27,7 @@ class ComputeClientV2(client.BaseClient): scopes = ComputeScopes default_scope_requirements = [Scope(ComputeScopes.all)] - def get_version(self, service: str | None = None) -> GlobusHTTPResponse: + def get_version(self, service: str | MissingType = MISSING) -> GlobusHTTPResponse: """Get the current version of the API and other services. :param service: Service for which to get version information. @@ -39,7 +40,7 @@ def get_version(self, service: str | None = None) -> GlobusHTTPResponse: :service: compute :ref: Root/operation/get_version_v2_version_get """ - query_params = {"service": service} if service else None + query_params = {"service": service} return self.get("/v2/version", query_params=query_params) def get_result_amqp_url(self) -> GlobusHTTPResponse: diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index befa6caf3..dcbda6c2c 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -5,6 +5,7 @@ from globus_sdk import client, response, utils from globus_sdk._types import UUIDLike from globus_sdk.scopes import GroupsScopes, Scope +from globus_sdk.utils import MISSING, MissingType from .data import BatchMembershipActions, GroupPolicies from .errors import GroupsAPIError @@ -58,7 +59,7 @@ def get_group( self, group_id: UUIDLike, *, - include: None | str | t.Iterable[str] = None, + include: str | t.Iterable[str] | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -80,10 +81,7 @@ def get_group( :service: groups :ref: get_group_v2_groups__group_id__get """ - if query_params is None: - query_params = {} - if include is not None: - query_params["include"] = ",".join(utils.safe_strseq_iter(include)) + query_params = {"include": utils.commajoin(include), **(query_params or {})} return self.get(f"/v2/groups/{group_id}", query_params=query_params) def get_group_by_subscription_id( From 17fc1dce73bc54b2bf3b59ce8dc2bb1baba23a25 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 5 Jun 2025 10:35:01 -0500 Subject: [PATCH 024/176] Bump version and changelog for release --- ...6_kurtmckee_rm_executable_version_code.rst | 12 ------ ...28_150526_sirosen_remove_mutable_scope.rst | 7 ---- ...22204_sirosen_remove_legacy_gare_alias.rst | 5 --- ...rosen_remove_deprecated_message_setter.rst | 5 --- ...irosen_remove_deprecated_timer_aliases.rst | 5 --- ...tuecke_sc_15807_flows_missing_defaults.rst | 4 -- ...uecke_sc_15807_search_missing_defaults.rst | 4 -- ..._15807_groups_compute_missing_defaults.rst | 5 --- ...x.tuecke_sc_15807_gcs_missing_defaults.rst | 4 -- changelog.rst | 39 +++++++++++++++++++ pyproject.toml | 2 +- 11 files changed, 40 insertions(+), 52 deletions(-) delete mode 100644 changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst delete mode 100644 changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst delete mode 100644 changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst delete mode 100644 changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst delete mode 100644 changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst delete mode 100644 changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst delete mode 100644 changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst delete mode 100644 changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst delete mode 100644 changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst diff --git a/changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst b/changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst deleted file mode 100644 index d10b66bda..000000000 --- a/changelog.d/20250520_121116_kurtmckee_rm_executable_version_code.rst +++ /dev/null @@ -1,12 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- The SDK version is no longer available in ``globus_sdk.version.__version__``. (:pr:`NUMBER`) - - Packages that want to query the SDK version must use ``importlib.metadata``: - - .. code-block:: python - - import importlib.metadata - - GLOBUS_SDK_VERSION = importlib.metadata.distribution("globus_sdk").version diff --git a/changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst b/changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst deleted file mode 100644 index 23e8e1ab5..000000000 --- a/changelog.d/20250528_150526_sirosen_remove_mutable_scope.rst +++ /dev/null @@ -1,7 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- The legacy ``MutableScope`` type has been removed. (:pr:`1198`) - - - The ``make_mutable`` method on ``ScopeBuilder`` objects has also been - removed as a consequence of this change. diff --git a/changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst b/changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst deleted file mode 100644 index 6685372f9..000000000 --- a/changelog.d/20250529_122204_sirosen_remove_legacy_gare_alias.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -~~~~~~~ - -- ``globus_sdk.experimental.auth_requirements_error`` has been removed. Use - ``globus_sdk.gare`` instead. (:pr:`1202`) diff --git a/changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst b/changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst deleted file mode 100644 index 4daa62886..000000000 --- a/changelog.d/20250530_102408_sirosen_remove_deprecated_message_setter.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -~~~~~~~ - -- ``GlobusAPIError`` no longer provides a setter for ``message``. The - ``message`` property is now read-only. (:pr:`NUMBER`) diff --git a/changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst b/changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst deleted file mode 100644 index 3383cbd50..000000000 --- a/changelog.d/20250530_154707_sirosen_remove_deprecated_timer_aliases.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -~~~~~~~ - -- Deprecated aliases for ``TimersClient``, ``TimersScopes``, and - ``TimersAPIError`` have been removed. (:pr:`NUMBER`) diff --git a/changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst b/changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst deleted file mode 100644 index b064c4c9f..000000000 --- a/changelog.d/20250530_163223_max.tuecke_sc_15807_flows_missing_defaults.rst +++ /dev/null @@ -1,4 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- All defaults of None converted to globus_sdk.MISSING for all payload types in the flows client. (:pr:`1205`) \ No newline at end of file diff --git a/changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst b/changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst deleted file mode 100644 index f34cabb4d..000000000 --- a/changelog.d/20250603_130159_max.tuecke_sc_15807_search_missing_defaults.rst +++ /dev/null @@ -1,4 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Search client. (:pr:`1207`) \ No newline at end of file diff --git a/changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst b/changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst deleted file mode 100644 index 35af6ca06..000000000 --- a/changelog.d/20250604_174013_max.tuecke_sc_15807_groups_compute_missing_defaults.rst +++ /dev/null @@ -1,5 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Compute client. (:pr:`1212`) -- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Groups client. (:pr:`1212`) \ No newline at end of file diff --git a/changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst b/changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst deleted file mode 100644 index 5695e1683..000000000 --- a/changelog.d/20250605_095240_max.tuecke_sc_15807_gcs_missing_defaults.rst +++ /dev/null @@ -1,4 +0,0 @@ -Breaking Changes -~~~~~~~~~~~~~~~~ - -- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the GCS client. (:pr:`1214`) \ No newline at end of file diff --git a/changelog.rst b/changelog.rst index d0c4f9f53..7b6730237 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,45 @@ to a major new version of the SDK. .. scriv-insert-here +.. _changelog-4.0.0a2: + +v4.0.0a2 (2025-06-05) +--------------------- + +Breaking Changes +~~~~~~~~~~~~~~~~ + +- The SDK version is no longer available in ``globus_sdk.version.__version__``. (:pr:`1195`) + + Packages that want to query the SDK version must use ``importlib.metadata``: + + .. code-block:: python + + import importlib.metadata + + GLOBUS_SDK_VERSION = importlib.metadata.distribution("globus_sdk").version + +- The legacy ``MutableScope`` type has been removed. (:pr:`1198`) + + - The ``make_mutable`` method on ``ScopeBuilder`` objects has also been + removed as a consequence of this change. + +- Defaults of ``None`` were converted to ``globus_sdk.MISSING`` for multiple client + methods and payload types, covering Compute, Flows, Groups, GCS, and Search. + (:pr:`1205`, :pr:`1207`, :pr:`1212`, :pr:`1214`) + +Removed +~~~~~~~ + +- ``globus_sdk.experimental.auth_requirements_error`` has been removed. Use + ``globus_sdk.gare`` instead. (:pr:`1202`) + +- ``GlobusAPIError`` no longer provides a setter for ``message``. The + ``message`` property is now read-only. (:pr:`1204`) + +- Deprecated aliases for ``TimersClient``, ``TimersScopes``, and + ``TimersAPIError`` have been removed. (:pr:`1206`) + .. _changelog-4.0.0a1: v4.0.0a1 (2025-05-20) diff --git a/pyproject.toml b/pyproject.toml index 6ab7cd8a1..a9b76c223 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "globus-sdk" -version = "4.0.0a1" +version = "4.0.0a2" authors = [ { name = "Globus Team", email = "support@globus.org" }, ] From 8ed05661cac63159e65137982bc9f9f2e328788f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 15 Apr 2025 15:58:42 -0500 Subject: [PATCH 025/176] Add `add_app_transfer_data_access_scope` for flows This is a new SpecificFlowClient method for manipulating a GlobusApp's scope registration data to add `data_access` requirements. --- ...specific_flow_data_access_scope_helper.rst | 7 ++ src/globus_sdk/services/flows/client.py | 79 ++++++++++++++++++- .../globus_app/test_client_integration.py | 11 +++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst diff --git a/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst b/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst new file mode 100644 index 000000000..c706ae634 --- /dev/null +++ b/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst @@ -0,0 +1,7 @@ +Added +~~~~~ + +- ``SpecificFlowClient`` has a new method, + ``add_app_transfer_data_access_scope`` which facilitates declaration of scope + requirements when starting flows which interact with collections that need + ``data_access`` scopes. (:pr:`1166`) diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 4663aefd3..4a2dd0af5 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -1,11 +1,14 @@ from __future__ import annotations import logging +import sys import typing as t +import uuid from globus_sdk import ( GlobusHTTPResponse, GlobusSDKUsageError, + _guards, client, exc, paging, @@ -14,7 +17,14 @@ from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp -from globus_sdk.scopes import FlowsScopes, Scope, ScopeBuilder, SpecificFlowScopeBuilder +from globus_sdk.scopes import ( + FlowsScopes, + GCSCollectionScopeBuilder, + Scope, + ScopeBuilder, + SpecificFlowScopeBuilder, + TransferScopes, +) from globus_sdk.utils import MISSING, MissingType from .data import RunActivityNotificationPolicy @@ -25,6 +35,11 @@ IterableRunsResponse, ) +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + log = logging.getLogger(__name__) @@ -940,6 +955,68 @@ def __init__( def default_scope_requirements(self) -> list[Scope]: return [Scope(self.scopes.user)] + def add_app_transfer_data_access_scope( + self, collection_ids: UUIDLike | t.Iterable[UUIDLike] + ) -> Self: + """ + Add a dependent ``data_access`` scope for one or more given ``collection_ids`` + to this client's ``GlobusApp``, under the Transfer ``all`` scope. + Useful for preventing ``ConsentRequired`` errors when starting or resuming runs + of flows that use Globus Connect Server mapped collection(s). + + .. warning:: + + This method must only be used on ``collection_ids`` for non-High-Assurance + GCS Mapped Collections. + + Use on other collection types, e.g., on GCP Mapped Collections or any form + of Guest Collection, will result in "Unknown Scope" errors during the login + flow. + + Returns ``self`` for chaining. + + Raises ``GlobusSDKUsageError`` if this client was not initialized with an app. + + :param collection_ids: a collection ID or an iterable of IDs. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: python + + flow_id = ... + COLLECTION_ID = ... + app = UserApp("myapp", client_id=NATIVE_APP_CLIENT_ID) + client = SpecificFlowClient(FLOW_ID, app=app).add_app_transfer_data_access_scope( + COLLECTION_ID + ) + + client.run_flow({"collection": COLLECTION_ID}) + """ # noqa: E501 + if isinstance(collection_ids, (str, uuid.UUID)): + _guards.validators.uuidlike("collection_ids", collection_ids) + # wrap the collection_ids input in a list for consistent iteration below + collection_ids_ = [collection_ids] + else: + # copy to a list so that ephemeral iterables can be iterated multiple times + collection_ids_ = list(collection_ids) + for i, c in enumerate(collection_ids_): + _guards.validators.uuidlike(f"collection_ids[{i}]", c) + + transfer_scope = Scope(TransferScopes.all, optional=True) + for coll_id in collection_ids_: + data_access_scope = Scope( + GCSCollectionScopeBuilder(str(coll_id)).data_access, + optional=True, + ) + transfer_scope.add_dependency(data_access_scope) + + specific_flow_scope = Scope(self.scopes.user) + specific_flow_scope.add_dependency(transfer_scope) + self.add_app_scope(specific_flow_scope) + return self + def run_flow( self, body: dict[str, t.Any], diff --git a/tests/unit/globus_app/test_client_integration.py b/tests/unit/globus_app/test_client_integration.py index 06998b00b..0e1433b29 100644 --- a/tests/unit/globus_app/test_client_integration.py +++ b/tests/unit/globus_app/test_client_integration.py @@ -64,6 +64,17 @@ def test_timers_client_add_app_data_access_scope(app): assert expected in str_list +def test_specific_flow_client_add_app_data_access_scope(app): + flow_id = str(uuid.UUID(int=1)) + client = globus_sdk.SpecificFlowClient(flow_id, app=app) + + collection_id = str(uuid.UUID(int=0)) + client.add_app_transfer_data_access_scope(collection_id) + str_list = [str(s) for s in app.scope_requirements[client.resource_server]] + expected = f"{client.scopes.user}[*urn:globus:auth:scope:transfer.api.globus.org:all[*https://auth.globus.org/scopes/{collection_id}/data_access]]" # noqa: E501 + assert expected in str_list + + def test_transfer_client_add_app_data_access_scope_chaining(app): collection_id_1 = str(uuid.UUID(int=1)) collection_id_2 = str(uuid.UUID(int=2)) From 04f15ea0883fe924baf8ff77b6d6ec72a36669ae Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 14:23:26 -0500 Subject: [PATCH 026/176] Updated transfer client to use MISSING defaults --- src/globus_sdk/services/transfer/client.py | 381 +++++++++--------- .../services/transfer/data/delete_data.py | 23 +- .../services/transfer/data/transfer_data.py | 53 ++- .../services/timers/test_create_timer.py | 5 +- .../services/transfer/test_operation_mkdir.py | 5 +- .../transfer/test_operation_rename.py | 5 +- .../services/transfer/test_task_list.py | 2 +- tests/unit/helpers/test_transfer.py | 25 +- 8 files changed, 253 insertions(+), 246 deletions(-) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index a046d1297..38b02e28f 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -8,6 +8,7 @@ from globus_sdk import _guards, client, exc, paging, response, utils from globus_sdk._types import DateLike, IntLike, UUIDLike from globus_sdk.scopes import GCSCollectionScopeBuilder, Scope, TransferScopes +from globus_sdk.utils import MISSING, MissingType from .data import DeleteData, TransferData from .errors import TransferAPIError @@ -23,15 +24,38 @@ def _datelike_to_str(x: DateLike) -> str: return x if isinstance(x, str) else x.isoformat(timespec="seconds") -def _format_filter_item(x: str | TransferFilterDict) -> str: - if isinstance(x, str): +def _format_completion_time( + x: str | tuple[DateLike, DateLike] | MissingType, +) -> str | MissingType: + if isinstance(x, MissingType): + return MISSING + elif isinstance(x, str): + return x + else: + start_t, end_t = x + start_t, end_t = _datelike_to_str(start_t), _datelike_to_str(end_t) + return f"{start_t},{end_t}" + + +@t.overload +def _format_filter_item(x: str | TransferFilterDict) -> str: ... + + +@t.overload +def _format_filter_item(x: MissingType) -> MissingType: ... + + +def _format_filter_item(x: str | TransferFilterDict | MissingType) -> str | MissingType: + if isinstance(x, MissingType): + return MISSING + elif isinstance(x, str): return x return "/".join(f"{k}:{utils.commajoin(v)}" for k, v in x.items()) def _format_filter( - x: str | TransferFilterDict | list[str | TransferFilterDict], -) -> str | list[str]: + x: str | TransferFilterDict | list[str | TransferFilterDict] | MissingType, +) -> str | list[str] | MissingType: if isinstance(x, list): return [_format_filter_item(y) for y in x] return _format_filter_item(x) @@ -381,12 +405,12 @@ def delete_endpoint(self, endpoint_id: UUIDLike) -> response.GlobusHTTPResponse: ) def endpoint_search( self, - filter_fulltext: str | None = None, + filter_fulltext: str | MissingType = MISSING, *, - filter_scope: str | None = None, - filter_owner_id: str | None = None, - filter_host_endpoint: UUIDLike | None = None, - filter_non_functional: bool | None = None, + filter_scope: str | MissingType = MISSING, + filter_owner_id: str | MissingType = MISSING, + filter_host_endpoint: UUIDLike | MissingType = MISSING, + filter_non_functional: bool | MissingType = MISSING, filter_entity_type: ( t.Literal[ "GCP_mapped_collection", @@ -395,10 +419,10 @@ def endpoint_search( "GCSv5_mapped_collection", "GCSv5_guest_collection", ] - | None - ) = None, - limit: int | None = None, - offset: int | None = None, + | MissingType + ) = MISSING, + limit: int | MissingType = MISSING, + offset: int | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: r""" @@ -461,24 +485,25 @@ def endpoint_search( .. extdoclink:: Endpoint and Collection Search :ref: transfer/endpoint_and_collection_search """ # noqa: E501 - if query_params is None: - query_params = {} - if filter_scope is not None: - query_params["filter_scope"] = filter_scope - if filter_fulltext is not None: - query_params["filter_fulltext"] = filter_fulltext - if filter_owner_id is not None: - query_params["filter_owner_id"] = filter_owner_id - if filter_host_endpoint is not None: - query_params["filter_host_endpoint"] = filter_host_endpoint - if filter_non_functional is not None: # convert to int (expect bool input) - query_params["filter_non_functional"] = 1 if filter_non_functional else 0 - if filter_entity_type is not None: - query_params["filter_entity_type"] = filter_entity_type - if limit is not None: - query_params["limit"] = limit - if offset is not None: - query_params["offset"] = offset + query_params = { + "filter_scope": filter_scope, + "filter_fulltext": filter_fulltext, + "filter_owner_id": filter_owner_id, + "filter_host_endpoint": filter_host_endpoint, + "filter_non_functional": ( + 1 + if filter_non_functional + else ( + 0 + if isinstance(filter_non_functional, bool) + else filter_non_functional + ) + ), + "filter_entity_type": filter_entity_type, + "limit": limit, + "offset": offset, + **(query_params or {}), + } log.debug(f"TransferClient.endpoint_search({query_params})") return IterableTransferResponse( self.get("/v0.10/endpoint_search", query_params=query_params) @@ -488,7 +513,7 @@ def endpoint_autoactivate( self, endpoint_id: UUIDLike, *, - if_expires_in: int | None = None, + if_expires_in: int | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: r""" @@ -504,10 +529,10 @@ def endpoint_autoactivate( :param query_params: Any additional parameters will be passed through as query params. """ # noqa: E501 - if query_params is None: - query_params = {} - if if_expires_in is not None: - query_params["if_expires_in"] = if_expires_in + query_params = { + "if_expires_in": if_expires_in, + **(query_params or {}), + } log.debug(f"TransferClient.endpoint_autoactivate({endpoint_id})") return self.post( f"/v0.10/endpoint/{endpoint_id}/autoactivate", query_params=query_params @@ -651,8 +676,8 @@ def get_shared_endpoint_list( self, endpoint_id: UUIDLike, *, - max_results: int | None = None, - next_token: str | None = None, + max_results: int | MissingType = MISSING, + next_token: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -678,12 +703,13 @@ def get_shared_endpoint_list( :ref: transfer/endpoints_and_collections/get_guest_collection_list """ log.debug(f"TransferClient.get_shared_endpoint_list({endpoint_id}, ...)") - if query_params is None: - query_params = {} - if max_results is not None: - query_params["max_results"] = str(max_results) - if next_token is not None: - query_params["next_token"] = next_token + query_params = { + "max_results": ( + str(max_results) if isinstance(max_results, int) else max_results + ), + "next_token": next_token, + **(query_params or {}), + } return IterableTransferResponse( self.get( f"/v0.10/endpoint/{endpoint_id}/shared_endpoint_list", @@ -1187,16 +1213,18 @@ def delete_bookmark(self, bookmark_id: UUIDLike) -> response.GlobusHTTPResponse: def operation_ls( self, endpoint_id: UUIDLike, - path: str | None = None, + path: str | MissingType = MISSING, *, - show_hidden: bool | None = None, - orderby: str | list[str] | None = None, - limit: int | None = None, - offset: int | None = None, + show_hidden: bool | MissingType = MISSING, + orderby: str | list[str] | MissingType = MISSING, + limit: int | MissingType = MISSING, + offset: int | MissingType = MISSING, # note: filter is a soft keyword in python, so using this name is okay # pylint: disable=redefined-builtin - filter: str | TransferFilterDict | list[str | TransferFilterDict] | None = None, - local_user: str | None = None, + filter: ( + str | TransferFilterDict | list[str | TransferFilterDict] | MissingType + ) = MISSING, + local_user: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -1271,26 +1299,20 @@ def operation_ls( .. extdoclink:: List Directory Contents :ref: transfer/file_operations/#list_directory_contents """ # noqa: E501 - if query_params is None: - query_params = {} - if path is not None: - query_params["path"] = path - if show_hidden is not None: - query_params["show_hidden"] = 1 if show_hidden else 0 - if limit is not None: - query_params["limit"] = limit - if offset is not None: - query_params["offset"] = offset - if orderby is not None: - if isinstance(orderby, str): - query_params["orderby"] = orderby - else: - query_params["orderby"] = ",".join(orderby) - if filter is not None: - query_params["filter"] = _format_filter(filter) - if local_user is not None: - query_params["local_user"] = local_user - + query_params = { + "path": path, + "limit": limit, + "offset": offset, + "show_hidden": ( + 1 + if show_hidden + else 0 if isinstance(show_hidden, bool) else show_hidden + ), + "orderby": utils.commajoin(orderby), + "filter": _format_filter(filter), + "local_user": local_user, + **(query_params or {}), + } log.debug(f"TransferClient.operation_ls({endpoint_id}, {query_params})") return IterableTransferResponse( self.get( @@ -1303,7 +1325,7 @@ def operation_mkdir( endpoint_id: UUIDLike, path: str, *, - local_user: str | None = None, + local_user: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -1335,9 +1357,11 @@ def operation_mkdir( endpoint_id, path, query_params ) ) - json_body = {"DATA_TYPE": "mkdir", "path": path} - if local_user is not None: - json_body["local_user"] = local_user + json_body = { + "DATA_TYPE": "mkdir", + "path": path, + "local_user": local_user, + } return self.post( f"/v0.10/operation/endpoint/{endpoint_id}/mkdir", data=json_body, @@ -1350,7 +1374,7 @@ def operation_rename( oldpath: str, newpath: str, *, - local_user: str | None = None, + local_user: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -1387,9 +1411,8 @@ def operation_rename( "DATA_TYPE": "rename", "old_path": oldpath, "new_path": newpath, + "local_user": local_user, } - if local_user is not None: - json_body["local_user"] = local_user return self.post( f"/v0.10/operation/endpoint/{endpoint_id}/rename", data=json_body, @@ -1399,9 +1422,9 @@ def operation_rename( def operation_stat( self, endpoint_id: UUIDLike, - path: str | None = None, + path: str | MissingType = MISSING, *, - local_user: str | None = None, + local_user: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -1432,13 +1455,11 @@ def operation_stat( .. extdoclink:: Get File or Directory Status :ref: transfer/file_operations/#stat """ - if query_params is None: - query_params = {} - if path is not None: - query_params["path"] = path - if local_user is not None: - query_params["local_user"] = local_user - + query_params = { + "path": path, + "local_user": local_user, + **(query_params or {}), + } log.debug(f"TransferClient.operation_stat({endpoint_id}, {query_params})") return self.get( f"/v0.10/operation/endpoint/{endpoint_id}/stat", query_params=query_params @@ -1560,7 +1581,7 @@ def submit_transfer( :ref: transfer/task_submit/#submit_transfer_task """ # noqa: E501 log.debug("TransferClient.submit_transfer(...)") - if "submission_id" not in data: + if isinstance(data.get("submission_id", MISSING), MissingType): log.debug("submit_transfer autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] return self.post("/v0.10/transfer", data=data) @@ -1603,7 +1624,7 @@ def submit_delete( :ref: transfer/task_submit/#submit_delete_task """ log.debug("TransferClient.submit_delete(...)") - if "submission_id" not in data: + if isinstance(data.get("submission_id", MISSING), MissingType): log.debug("submit_delete autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] return self.post("/v0.10/delete", data=data) @@ -1622,11 +1643,11 @@ def submit_delete( def task_list( self, *, - limit: int | None = None, - offset: int | None = None, - orderby: str | list[str] | None = None, + limit: int | MissingType = MISSING, + offset: int | MissingType = MISSING, + orderby: str | list[str] | MissingType = MISSING, # pylint: disable=redefined-builtin - filter: str | TransferFilterDict | None = None, + filter: str | TransferFilterDict | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -1702,19 +1723,13 @@ def task_list( :ref: transfer/task/#get_task_list """ # noqa: E501 log.debug("TransferClient.task_list(...)") - if query_params is None: - query_params = {} - if limit is not None: - query_params["limit"] = limit - if offset is not None: - query_params["offset"] = offset - if orderby is not None: - if isinstance(orderby, str): - query_params["orderby"] = orderby - else: - query_params["orderby"] = ",".join(orderby) - if filter is not None: - query_params["filter"] = _format_filter_item(filter) + query_params = { + "limit": limit, + "offset": offset, + "orderby": utils.commajoin(orderby), + "filter": _format_filter_item(filter), + **(query_params or {}), + } return IterableTransferResponse( self.get("/v0.10/task_list", query_params=query_params) ) @@ -1730,8 +1745,8 @@ def task_event_list( self, task_id: UUIDLike, *, - limit: int | None = None, - offset: int | None = None, + limit: int | MissingType = MISSING, + offset: int | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: r""" @@ -1767,12 +1782,11 @@ def task_event_list( :ref: transfer/task/#get_event_list """ # noqa: E501 log.debug(f"TransferClient.task_event_list({task_id}, ...)") - if query_params is None: - query_params = {} - if limit is not None: - query_params["limit"] = limit - if offset is not None: - query_params["offset"] = offset + query_params = { + "limit": limit, + "offset": offset, + **(query_params or {}), + } return IterableTransferResponse( self.get(f"/v0.10/task/{task_id}/event_list", query_params=query_params) ) @@ -1978,7 +1992,7 @@ def task_successful_transfers( self, task_id: UUIDLike, *, - marker: str | None = None, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -2020,10 +2034,10 @@ def task_successful_transfers( :ref: transfer/task/#get_task_successful_transfers """ # noqa: E501 log.debug(f"TransferClient.task_successful_transfers({task_id}, ...)") - if query_params is None: - query_params = {} - if marker is not None: - query_params["marker"] = marker + query_params = { + "marker": marker, + **(query_params or {}), + } return IterableTransferResponse( self.get( f"/v0.10/task/{task_id}/successful_transfers", query_params=query_params @@ -2037,7 +2051,7 @@ def task_skipped_errors( self, task_id: UUIDLike, *, - marker: str | None = None, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -2073,10 +2087,10 @@ def task_skipped_errors( :ref: transfer/task/#get_task_skipped_errors """ # noqa: E501 log.debug("TransferClient.task_skipped_errors(%s, ...)", task_id) - if query_params is None: - query_params = {} - if marker is not None: - query_params["marker"] = marker + query_params = { + "marker": marker, + **(query_params or {}), + } return IterableTransferResponse( self.get(f"/v0.10/task/{task_id}/skipped_errors", query_params=query_params) ) @@ -2207,16 +2221,16 @@ def endpoint_manager_acl_list( def endpoint_manager_task_list( self, *, - filter_status: None | str | t.Iterable[str] = None, - filter_task_id: None | UUIDLike | t.Iterable[UUIDLike] = None, - filter_owner_id: UUIDLike | None = None, - filter_endpoint: UUIDLike | None = None, - filter_endpoint_use: t.Literal["source", "destination"] | None = None, - filter_is_paused: bool | None = None, - filter_completion_time: None | str | tuple[DateLike, DateLike] = None, - filter_min_faults: int | None = None, - filter_local_user: str | None = None, - last_key: str | None = None, + filter_status: str | t.Iterable[str] | MissingType = MISSING, + filter_task_id: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, + filter_owner_id: UUIDLike | MissingType = MISSING, + filter_endpoint: UUIDLike | MissingType = MISSING, + filter_endpoint_use: t.Literal["source", "destination"] | MissingType = MISSING, + filter_is_paused: bool | MissingType = MISSING, + filter_completion_time: str | tuple[DateLike, DateLike] | MissingType = MISSING, + filter_min_faults: int | MissingType = MISSING, + filter_local_user: str | MissingType = MISSING, + last_key: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: r""" @@ -2324,39 +2338,26 @@ def endpoint_manager_task_list( :ref: transfer/advanced_collection_management/#get_tasks """ # noqa: E501 log.debug("TransferClient.endpoint_manager_task_list(...)") - if filter_endpoint is None and filter_endpoint_use is not None: + if isinstance(filter_endpoint, MissingType) and not isinstance( + filter_endpoint_use, MissingType + ): raise exc.GlobusSDKUsageError( "`filter_endpoint_use` is only valid when `filter_endpoint` is " "also supplied." ) - - if query_params is None: - query_params = {} - if filter_status is not None: - query_params["filter_status"] = utils.commajoin(filter_status) - if filter_task_id is not None: - query_params["filter_task_id"] = utils.commajoin(filter_task_id) - if filter_owner_id is not None: - query_params["filter_owner_id"] = filter_owner_id - if filter_endpoint is not None: - query_params["filter_endpoint"] = filter_endpoint - if filter_endpoint_use is not None: - query_params["filter_endpoint_use"] = filter_endpoint_use - if filter_is_paused is not None: - query_params["filter_is_paused"] = filter_is_paused - if filter_completion_time is not None: - if isinstance(filter_completion_time, str): - query_params["filter_completion_time"] = filter_completion_time - else: - start_t, end_t = filter_completion_time - start_t, end_t = _datelike_to_str(start_t), _datelike_to_str(end_t) - query_params["filter_completion_time"] = f"{start_t},{end_t}" - if filter_min_faults is not None: - query_params["filter_min_faults"] = filter_min_faults - if filter_local_user is not None: - query_params["filter_local_user"] = filter_local_user - if last_key is not None: - query_params["last_key"] = last_key + query_params = { + "filter_status": utils.commajoin(filter_status), + "filter_task_id": utils.commajoin(filter_task_id), + "filter_owner_id": filter_owner_id, + "filter_endpoint": filter_endpoint, + "filter_endpoint_use": filter_endpoint_use, + "filter_is_paused": filter_is_paused, + "filter_completion_time": _format_completion_time(filter_completion_time), + "filter_min_faults": filter_min_faults, + "filter_local_user": filter_local_user, + "last_key": last_key, + **(query_params or {}), + } return IterableTransferResponse( self.get("/v0.10/endpoint_manager/task_list", query_params=query_params) ) @@ -2399,9 +2400,9 @@ def endpoint_manager_task_event_list( self, task_id: UUIDLike, *, - limit: int | None = None, - offset: int | None = None, - filter_is_error: bool | None = None, + limit: int | MissingType = MISSING, + offset: int | MissingType = MISSING, + filter_is_error: bool | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -2431,14 +2432,16 @@ def endpoint_manager_task_event_list( :ref: transfer/advanced_collection_management/#get_task_events """ log.debug(f"TransferClient.endpoint_manager_task_event_list({task_id}, ...)") - if query_params is None: - query_params = {} - if limit is not None: - query_params["limit"] = limit - if offset is not None: - query_params["offset"] = offset - if filter_is_error is not None: - query_params["filter_is_error"] = 1 if filter_is_error else 0 + query_params = { + "limit": limit, + "offset": offset, + "filter_is_error": ( + 1 + if filter_is_error + else 0 if isinstance(filter_is_error, bool) else filter_is_error + ), + **(query_params or {}), + } return IterableTransferResponse( self.get( f"/v0.10/endpoint_manager/task/{task_id}/event_list", @@ -2481,7 +2484,7 @@ def endpoint_manager_task_successful_transfers( self, task_id: UUIDLike, *, - marker: str | None = None, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: r""" @@ -2509,10 +2512,10 @@ def endpoint_manager_task_successful_transfers( "TransferClient.endpoint_manager_task_successful_transfers(%s, ...)", task_id, ) - if query_params is None: - query_params = {} - if marker is not None: - query_params["marker"] = marker + query_params = { + "marker": marker, + **(query_params or {}), + } return IterableTransferResponse( self.get( f"/v0.10/endpoint_manager/task/{task_id}/successful_transfers", @@ -2527,7 +2530,7 @@ def endpoint_manager_task_skipped_errors( self, task_id: UUIDLike, *, - marker: str | None = None, + marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: r""" @@ -2554,10 +2557,10 @@ def endpoint_manager_task_skipped_errors( log.debug( f"TransferClient.endpoint_manager_task_skipped_errors({task_id}, ...)" ) - if query_params is None: - query_params = {} - if marker is not None: - query_params["marker"] = marker + query_params = { + "marker": marker, + **(query_params or {}), + } return IterableTransferResponse( self.get( f"/v0.10/endpoint_manager/task/{task_id}/skipped_errors", @@ -2695,7 +2698,7 @@ def endpoint_manager_resume_tasks( def endpoint_manager_pause_rule_list( self, *, - filter_endpoint: UUIDLike | None = None, + filter_endpoint: UUIDLike | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -2717,10 +2720,10 @@ def endpoint_manager_pause_rule_list( :ref: transfer/advanced_collection_management/#get_pause_rules """ log.debug("TransferClient.endpoint_manager_pause_rule_list(...)") - if query_params is None: - query_params = {} - if filter_endpoint is not None: - query_params["filter_endpoint"] = filter_endpoint + query_params = { + "filter_endpoint": filter_endpoint, + **(query_params or {}), + } return IterableTransferResponse( self.get( "/v0.10/endpoint_manager/pause_rule_list", query_params=query_params diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index 22a7792ea..3dedbd982 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -6,6 +6,7 @@ from globus_sdk import exc, utils from globus_sdk._types import UUIDLike +from globus_sdk.utils import MISSING, MissingType if t.TYPE_CHECKING: import globus_sdk @@ -87,25 +88,25 @@ class DeleteData(utils.PayloadWrapper): def __init__( self, transfer_client: globus_sdk.TransferClient | None = None, - endpoint: UUIDLike | None = None, + endpoint: UUIDLike | MissingType = MISSING, *, - label: str | None = None, - submission_id: UUIDLike | None = None, + label: str | MissingType = MISSING, + submission_id: UUIDLike | MissingType = MISSING, recursive: bool = False, ignore_missing: bool = False, interpret_globs: bool = False, - deadline: str | datetime.datetime | None = None, - skip_activation_check: bool | None = None, + deadline: str | datetime.datetime | MissingType = MISSING, + skip_activation_check: bool | MissingType = MISSING, notify_on_succeeded: bool = True, notify_on_failed: bool = True, notify_on_inactive: bool = True, - local_user: str | None = None, + local_user: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() # this must be checked explicitly to handle the fact that `transfer_client` is # the first arg - if endpoint is None: + if isinstance(endpoint, MissingType): raise exc.GlobusSDKUsageError("endpoint is required") self["DATA_TYPE"] = "delete" @@ -160,9 +161,11 @@ def add_item( :param path: Path to the directory or file to be deleted :param additional_fields: additional fields to be added to the delete item """ - item_data = {"DATA_TYPE": "delete_item", "path": path} - if additional_fields is not None: - item_data.update(additional_fields) + item_data = { + "DATA_TYPE": "delete_item", + "path": path, + **(additional_fields or {}), + } log.debug('DeleteData[{}].add_item: "{}"'.format(self["endpoint"], path)) self["DATA"].append(item_data) diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index a6d742392..d93c4d79b 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -6,6 +6,7 @@ from globus_sdk import exc, utils from globus_sdk._types import UUIDLike +from globus_sdk.utils import MISSING, MissingType if t.TYPE_CHECKING: import globus_sdk @@ -164,36 +165,36 @@ class TransferData(utils.PayloadWrapper): def __init__( self, transfer_client: globus_sdk.TransferClient | None = None, - source_endpoint: UUIDLike | None = None, - destination_endpoint: UUIDLike | None = None, + source_endpoint: UUIDLike | MissingType = MISSING, + destination_endpoint: UUIDLike | MissingType = MISSING, *, - label: str | None = None, - submission_id: UUIDLike | None = None, + label: str | MissingType = MISSING, + submission_id: UUIDLike | MissingType = MISSING, sync_level: ( - int | None | t.Literal["exists", "size", "mtime", "checksum"] - ) = None, + int | t.Literal["exists", "size", "mtime", "checksum"] | MissingType + ) = MISSING, verify_checksum: bool = False, preserve_timestamp: bool = False, encrypt_data: bool = False, - deadline: datetime.datetime | str | None = None, - skip_activation_check: bool | None = None, + deadline: datetime.datetime | str | MissingType = MISSING, + skip_activation_check: bool | MissingType = MISSING, skip_source_errors: bool = False, fail_on_quota_errors: bool = False, - recursive_symlinks: str | None = None, + recursive_symlinks: str | MissingType = MISSING, delete_destination_extra: bool = False, notify_on_succeeded: bool = True, notify_on_failed: bool = True, notify_on_inactive: bool = True, - source_local_user: str | None = None, - destination_local_user: str | None = None, + source_local_user: str | MissingType = MISSING, + destination_local_user: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() # these must be checked explicitly to handle the fact that `transfer_client` is # the first arg - if source_endpoint is None: + if isinstance(source_endpoint, MissingType): raise exc.GlobusSDKUsageError("source_endpoint is required") - if destination_endpoint is None: + if isinstance(destination_endpoint, MissingType): raise exc.GlobusSDKUsageError("destination_endpoint is required") log.debug("Creating a new TransferData object") @@ -244,9 +245,9 @@ def add_item( source_path: str, destination_path: str, *, - recursive: bool | None = None, - external_checksum: str | None = None, - checksum_algorithm: str | None = None, + recursive: bool | MissingType = MISSING, + external_checksum: str | MissingType = MISSING, + checksum_algorithm: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: """ @@ -282,16 +283,11 @@ def add_item( "DATA_TYPE": "transfer_item", "source_path": source_path, "destination_path": destination_path, + "recursive": recursive, + "external_checksum": external_checksum, + "checksum_algorithm": checksum_algorithm, + **(additional_fields or {}), } - if recursive is not None: - item_data["recursive"] = recursive - if external_checksum is not None: - item_data["external_checksum"] = external_checksum - if checksum_algorithm is not None: - item_data["checksum_algorithm"] = checksum_algorithm - if additional_fields is not None: - item_data.update(additional_fields) - log.debug( 'TransferData[{}, {}].add_item: "{}"->"{}"'.format( self["source_endpoint"], @@ -334,8 +330,8 @@ def add_filter_rule( *, method: t.Literal["include", "exclude"] = "exclude", type: ( # pylint: disable=redefined-builtin - None | t.Literal["file", "dir"] - ) = None, + t.Literal["file", "dir"] | MissingType + ) = MISSING, ) -> None: """ Add a filter rule to the transfer document. @@ -383,9 +379,8 @@ def add_filter_rule( "DATA_TYPE": "filter_rule", "method": method, "name": name, + "type": type, } - if type is not None: - rule["type"] = type self["filter_rules"].append(rule) def iter_items(self) -> t.Iterator[dict[str, t.Any]]: diff --git a/tests/functional/services/timers/test_create_timer.py b/tests/functional/services/timers/test_create_timer.py index e5faf2f00..4689b63a3 100644 --- a/tests/functional/services/timers/test_create_timer.py +++ b/tests/functional/services/timers/test_create_timer.py @@ -2,6 +2,7 @@ import globus_sdk from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import filter_missing def test_dummy_timer_creation(client): @@ -44,5 +45,7 @@ def test_transfer_timer_creation(client): "end": {"condition": "iterations", "iterations": 3}, } assert sent["timer"]["body"] == { - k: v for k, v in body.items() if k != "skip_activation_check" + k: [filter_missing(data_val) for data_val in v] if k == "DATA" else v + for k, v in filter_missing(body).items() + if k != "skip_activation_check" } diff --git a/tests/functional/services/transfer/test_operation_mkdir.py b/tests/functional/services/transfer/test_operation_mkdir.py index 82d812d3a..39cbbbe13 100644 --- a/tests/functional/services/transfer/test_operation_mkdir.py +++ b/tests/functional/services/transfer/test_operation_mkdir.py @@ -4,11 +4,12 @@ import pytest from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING _OMIT = object() -@pytest.mark.parametrize("local_user", ("my-user", None, _OMIT)) +@pytest.mark.parametrize("local_user", ("my-user", MISSING, _OMIT)) def test_operation_mkdir(client, local_user): meta = load_response(client.operation_mkdir).metadata endpoint_id = meta["endpoint_id"] @@ -32,7 +33,7 @@ def test_operation_mkdir(client, local_user): req = get_last_request() body = json.loads(req.body) assert body["path"] == "~/dir/" - if local_user not in (_OMIT, None): + if local_user not in (_OMIT, MISSING): assert body["local_user"] == local_user else: assert "local_user" not in body diff --git a/tests/functional/services/transfer/test_operation_rename.py b/tests/functional/services/transfer/test_operation_rename.py index 53a7a2866..7610c3a58 100644 --- a/tests/functional/services/transfer/test_operation_rename.py +++ b/tests/functional/services/transfer/test_operation_rename.py @@ -4,11 +4,12 @@ import pytest from globus_sdk._testing import get_last_request, load_response +from globus_sdk.utils import MISSING _OMIT = object() -@pytest.mark.parametrize("local_user", ("my-user", None, _OMIT)) +@pytest.mark.parametrize("local_user", ("my-user", MISSING, _OMIT)) def test_operation_rename(client, local_user): meta = load_response(client.operation_rename).metadata endpoint_id = meta["endpoint_id"] @@ -35,7 +36,7 @@ def test_operation_rename(client, local_user): body = json.loads(req.body) assert body["old_path"] == "~/old-name" assert body["new_path"] == "~/new-name" - if local_user not in (_OMIT, None): + if local_user not in (_OMIT, MISSING): assert body["local_user"] == local_user else: assert "local_user" not in body diff --git a/tests/functional/services/transfer/test_task_list.py b/tests/functional/services/transfer/test_task_list.py index 4d4d3378a..c066c60a1 100644 --- a/tests/functional/services/transfer/test_task_list.py +++ b/tests/functional/services/transfer/test_task_list.py @@ -12,7 +12,7 @@ ({"query_params": {"foo": "bar"}}, {"foo": "bar"}), ({"filter": "foo"}, {"filter": "foo"}), ({"limit": 10, "offset": 100}, {"limit": "10", "offset": "100"}), - ({"limit": 10, "query_params": {"limit": 100}}, {"limit": "10"}), + ({"limit": 10, "query_params": {"limit": 100}}, {"limit": "100"}), ({"filter": "foo:bar:baz"}, {"filter": "foo:bar:baz"}), ({"filter": {"foo": "bar", "bar": "baz"}}, {"filter": "foo:bar/bar:baz"}), ({"filter": {"foo": ["bar", "baz"]}}, {"filter": "foo:bar,baz"}), diff --git a/tests/unit/helpers/test_transfer.py b/tests/unit/helpers/test_transfer.py index cb5875a85..67b89649f 100644 --- a/tests/unit/helpers/test_transfer.py +++ b/tests/unit/helpers/test_transfer.py @@ -3,6 +3,7 @@ from globus_sdk import DeleteData, GlobusSDKUsageError, TransferClient, TransferData from globus_sdk._testing import load_response from globus_sdk.services.transfer.client import _format_filter +from globus_sdk.utils import MISSING from tests.common import GO_EP1_ID, GO_EP2_ID @@ -65,9 +66,9 @@ def test_transfer_init_no_client(): [ (), (GO_EP1_ID, GO_EP2_ID), - (None, None, None), - (None, GO_EP1_ID, None), - (None, None, GO_EP2_ID), + (MISSING, MISSING, MISSING), + (MISSING, GO_EP1_ID, MISSING), + (MISSING, MISSING, GO_EP2_ID), ], ) def test_transfer_init_rejects_bad_usage(tdata_args): @@ -90,9 +91,9 @@ def test_transfer_add_item(): assert data["DATA_TYPE"] == "transfer_item" assert data["source_path"] == source_path assert data["destination_path"] == dest_path - assert "recursive" not in data - assert "external_checksum" not in data - assert "checksum_algorithm" not in data + assert data["recursive"] == MISSING + assert data["external_checksum"] == MISSING + assert data["checksum_algorithm"] == MISSING # add recursive item tdata.add_item(source_path, dest_path, recursive=True) @@ -103,8 +104,8 @@ def test_transfer_add_item(): assert r_data["source_path"] == source_path assert r_data["destination_path"] == dest_path assert r_data["recursive"] - assert "external_checksum" not in data - assert "checksum_algorithm" not in data + assert data["external_checksum"] == MISSING + assert data["checksum_algorithm"] == MISSING # item with checksum checksum = "d577273ff885c3f84dadb8578bb41399" @@ -117,7 +118,7 @@ def test_transfer_add_item(): assert c_data["DATA_TYPE"] == "transfer_item" assert c_data["source_path"] == source_path assert c_data["destination_path"] == dest_path - assert "recursive" not in c_data + assert c_data["recursive"] == MISSING assert c_data["external_checksum"] == checksum assert c_data["checksum_algorithm"] == algorithm @@ -129,7 +130,7 @@ def test_transfer_add_item(): assert fields_data["DATA_TYPE"] == "transfer_item" assert fields_data["source_path"] == source_path assert fields_data["destination_path"] == dest_path - assert "recursive" not in fields_data + assert fields_data["recursive"] == MISSING assert all(fields_data[k] == v for k, v in addfields.items()) @@ -204,7 +205,7 @@ def test_delete_init_no_client(args, kwargs): @pytest.mark.parametrize( - "ddata_args", [(), (GO_EP1_ID,), (None, None), (GO_EP1_ID, None)] + "ddata_args", [(), (GO_EP1_ID,), (MISSING, MISSING), (GO_EP1_ID, MISSING)] ) def test_delete_init_rejects_bad_usage(ddata_args): with pytest.raises(GlobusSDKUsageError): @@ -381,7 +382,7 @@ def test_add_filter_rule(): assert tdata["filter_rules"][1]["DATA_TYPE"] == "filter_rule" assert tdata["filter_rules"][1]["method"] == "exclude" assert tdata["filter_rules"][1]["name"] == "tmp" - assert "type" not in tdata["filter_rules"][1] + assert tdata["filter_rules"][1]["type"] == MISSING @pytest.mark.parametrize( From 93ae45d66ad42195d1e71b1456e93a40db8064d5 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 14:27:21 -0500 Subject: [PATCH 027/176] Added changelog --- ...5_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst diff --git a/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst b/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst new file mode 100644 index 000000000..32918f0ba --- /dev/null +++ b/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst @@ -0,0 +1,4 @@ +Breaking Changes +~~~~~~~~~~~~~~~~ + +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the transfer client. (:pr:`1216`) \ No newline at end of file From 6fd0ee846a4834d295569c475bbf3548731c72b5 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 16:36:33 -0500 Subject: [PATCH 028/176] Requested fix: changelog formatting Co-authored-by: Kurt McKee --- ...605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst b/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst index 32918f0ba..babe781a2 100644 --- a/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst +++ b/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst @@ -1,4 +1,4 @@ Breaking Changes ~~~~~~~~~~~~~~~~ -- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the transfer client. (:pr:`1216`) \ No newline at end of file +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Transfer client. (:pr:`1216`) \ No newline at end of file From 624ad1c74372379fe789d2a04a93b10e1e09c1d6 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 5 Jun 2025 16:49:26 -0500 Subject: [PATCH 029/176] Requested change: revert submission_id check --- src/globus_sdk/services/transfer/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 38b02e28f..35533e08b 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -1581,7 +1581,7 @@ def submit_transfer( :ref: transfer/task_submit/#submit_transfer_task """ # noqa: E501 log.debug("TransferClient.submit_transfer(...)") - if isinstance(data.get("submission_id", MISSING), MissingType): + if "submission_id" not in data: log.debug("submit_transfer autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] return self.post("/v0.10/transfer", data=data) @@ -1624,7 +1624,7 @@ def submit_delete( :ref: transfer/task_submit/#submit_delete_task """ log.debug("TransferClient.submit_delete(...)") - if isinstance(data.get("submission_id", MISSING), MissingType): + if "submission_id" not in data: log.debug("submit_delete autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] return self.post("/v0.10/delete", data=data) From 644ae89320a38a2f8b8f6f95383cd7b65df0a692 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Fri, 6 Jun 2025 11:14:22 -0500 Subject: [PATCH 030/176] Requested change: remove SDK defaults --- .../services/transfer/data/delete_data.py | 12 +++++------ .../services/transfer/data/transfer_data.py | 20 +++++++++---------- tests/unit/helpers/test_transfer.py | 12 +++++++---- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index 3dedbd982..94d9e05b8 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -92,14 +92,14 @@ def __init__( *, label: str | MissingType = MISSING, submission_id: UUIDLike | MissingType = MISSING, - recursive: bool = False, - ignore_missing: bool = False, - interpret_globs: bool = False, + recursive: bool | MissingType = MISSING, + ignore_missing: bool | MissingType = MISSING, + interpret_globs: bool | MissingType = MISSING, deadline: str | datetime.datetime | MissingType = MISSING, skip_activation_check: bool | MissingType = MISSING, - notify_on_succeeded: bool = True, - notify_on_failed: bool = True, - notify_on_inactive: bool = True, + notify_on_succeeded: bool | MissingType = MISSING, + notify_on_failed: bool | MissingType = MISSING, + notify_on_inactive: bool | MissingType = MISSING, local_user: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index d93c4d79b..e1dc18ad1 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -173,18 +173,18 @@ def __init__( sync_level: ( int | t.Literal["exists", "size", "mtime", "checksum"] | MissingType ) = MISSING, - verify_checksum: bool = False, - preserve_timestamp: bool = False, - encrypt_data: bool = False, + verify_checksum: bool | MissingType = MISSING, + preserve_timestamp: bool | MissingType = MISSING, + encrypt_data: bool | MissingType = MISSING, deadline: datetime.datetime | str | MissingType = MISSING, skip_activation_check: bool | MissingType = MISSING, - skip_source_errors: bool = False, - fail_on_quota_errors: bool = False, + skip_source_errors: bool | MissingType = MISSING, + fail_on_quota_errors: bool | MissingType = MISSING, recursive_symlinks: str | MissingType = MISSING, - delete_destination_extra: bool = False, - notify_on_succeeded: bool = True, - notify_on_failed: bool = True, - notify_on_inactive: bool = True, + delete_destination_extra: bool | MissingType = MISSING, + notify_on_succeeded: bool | MissingType = MISSING, + notify_on_failed: bool | MissingType = MISSING, + notify_on_inactive: bool | MissingType = MISSING, source_local_user: str | MissingType = MISSING, destination_local_user: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, @@ -328,7 +328,7 @@ def add_filter_rule( self, name: str, *, - method: t.Literal["include", "exclude"] = "exclude", + method: t.Literal["include", "exclude"] | MissingType = MISSING, type: ( # pylint: disable=redefined-builtin t.Literal["file", "dir"] | MissingType ) = MISSING, diff --git a/tests/unit/helpers/test_transfer.py b/tests/unit/helpers/test_transfer.py index 67b89649f..55b00cbb4 100644 --- a/tests/unit/helpers/test_transfer.py +++ b/tests/unit/helpers/test_transfer.py @@ -303,8 +303,12 @@ def _default(x): "notify_on_inactive": _default(n_inactive), } for k, v in expect.items(): - assert tdata[k] is v - assert ddata[k] is v + if k in notify_kwargs: + assert tdata[k] is v + assert ddata[k] is v + else: + assert k not in tdata + assert k not in ddata @pytest.mark.parametrize( @@ -368,7 +372,7 @@ def test_add_filter_rule(): tdata = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) assert "filter_rules" not in tdata - tdata.add_filter_rule("*.tgz", type="file") + tdata.add_filter_rule("*.tgz", type="file", method="exclude") assert "filter_rules" in tdata assert isinstance(tdata["filter_rules"], list) assert len(tdata["filter_rules"]) == 1 @@ -380,7 +384,7 @@ def test_add_filter_rule(): tdata.add_filter_rule("tmp") assert len(tdata["filter_rules"]) == 2 assert tdata["filter_rules"][1]["DATA_TYPE"] == "filter_rule" - assert tdata["filter_rules"][1]["method"] == "exclude" + assert tdata["filter_rules"][1]["method"] == MISSING assert tdata["filter_rules"][1]["name"] == "tmp" assert tdata["filter_rules"][1]["type"] == MISSING From 4eadaf4404f699dc7ec5c3b5cb24e764e648635d Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Fri, 6 Jun 2025 16:33:13 -0500 Subject: [PATCH 031/176] Requested change: revert filter rule method default --- src/globus_sdk/services/transfer/data/transfer_data.py | 2 +- tests/unit/helpers/test_transfer.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index e1dc18ad1..8f28fddce 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -328,7 +328,7 @@ def add_filter_rule( self, name: str, *, - method: t.Literal["include", "exclude"] | MissingType = MISSING, + method: t.Literal["include", "exclude"] = "exclude", type: ( # pylint: disable=redefined-builtin t.Literal["file", "dir"] | MissingType ) = MISSING, diff --git a/tests/unit/helpers/test_transfer.py b/tests/unit/helpers/test_transfer.py index 55b00cbb4..7efb1d8da 100644 --- a/tests/unit/helpers/test_transfer.py +++ b/tests/unit/helpers/test_transfer.py @@ -372,7 +372,7 @@ def test_add_filter_rule(): tdata = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) assert "filter_rules" not in tdata - tdata.add_filter_rule("*.tgz", type="file", method="exclude") + tdata.add_filter_rule("*.tgz", type="file") assert "filter_rules" in tdata assert isinstance(tdata["filter_rules"], list) assert len(tdata["filter_rules"]) == 1 @@ -384,7 +384,7 @@ def test_add_filter_rule(): tdata.add_filter_rule("tmp") assert len(tdata["filter_rules"]) == 2 assert tdata["filter_rules"][1]["DATA_TYPE"] == "filter_rule" - assert tdata["filter_rules"][1]["method"] == MISSING + assert tdata["filter_rules"][1]["method"] == "exclude" assert tdata["filter_rules"][1]["name"] == "tmp" assert tdata["filter_rules"][1]["type"] == MISSING From 7ea88fa8698c7c5449015b951423df16ec20a453 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Sat, 7 Jun 2025 00:47:44 -0500 Subject: [PATCH 032/176] Convert the parser perf test to pytest-benchmark This is an experiment in the use of `pytest-benchmark` to provide a structure for benchmarking in general. The perf test is moved and rephrased as a set of pytest test cases using the `benchmark` fixture. A new testenv, `tox r -e benchmark`, is defined to allow runs of these benchmarks. The `tests/benchmark/` dir is listed in norecursedirs, meaning that we don't need `pytest-benchmark` installed at all outside of the benchmark testing tox testenv. --- pyproject.toml | 2 +- tests/benchmark/__init__.py | 0 tests/benchmark/test_scope_parser.py | 32 +++++++++ .../performance/parser_benchmark.py | 70 ------------------- tox.ini | 6 ++ 5 files changed, 39 insertions(+), 71 deletions(-) create mode 100644 tests/benchmark/__init__.py create mode 100644 tests/benchmark/test_scope_parser.py delete mode 100644 tests/non-pytest/performance/parser_benchmark.py diff --git a/pyproject.toml b/pyproject.toml index a9b76c223..200ed6370 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ globus_sdk = [ [tool.pytest.ini_options] addopts = "--no-success-flaky-report --color=yes" testpaths = ["tests"] -norecursedirs = ["tests/non-pytest"] +norecursedirs = ["tests/non-pytest", "tests/benchmark"] filterwarnings = [ "error", ] diff --git a/tests/benchmark/__init__.py b/tests/benchmark/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/benchmark/test_scope_parser.py b/tests/benchmark/test_scope_parser.py new file mode 100644 index 000000000..45313c1a3 --- /dev/null +++ b/tests/benchmark/test_scope_parser.py @@ -0,0 +1,32 @@ +import pytest + +from globus_sdk.scopes import Scope + + +def _make_deep_scope(depth): + big_scope = "" + for i in range(depth): + big_scope += f"foo{i}[" + big_scope += "bar" + for _ in range(depth): + big_scope += "]" + return big_scope + + +def _make_wide_scope(width): + big_scope = "" + for i in range(width): + big_scope += f"foo{i} " + return big_scope + + +@pytest.mark.parametrize("depth", (10, 100, 1000, 2000, 3000, 4000, 5000)) +def test_deep_scope_parsing(benchmark, depth): + scope_string = _make_deep_scope(depth) + benchmark(Scope.parse, scope_string) + + +@pytest.mark.parametrize("width", (5000, 10000)) +def test_wide_scope_parsing(benchmark, width): + scope_string = _make_wide_scope(width) + benchmark(Scope.parse, scope_string) diff --git a/tests/non-pytest/performance/parser_benchmark.py b/tests/non-pytest/performance/parser_benchmark.py deleted file mode 100644 index 3a8e685e7..000000000 --- a/tests/non-pytest/performance/parser_benchmark.py +++ /dev/null @@ -1,70 +0,0 @@ -import timeit - - -def timeit_test() -> None: - for size, num_iterations, style in ( - (10, 1000, "deep"), - (100, 1000, "deep"), - (1000, 1000, "deep"), - (2000, 100, "deep"), - (3000, 100, "deep"), - (4000, 100, "deep"), - (5000, 100, "deep"), - (5000, 1000, "wide"), - (10000, 1000, "wide"), - ): - if style == "deep": - setup = f"""\ -from globus_sdk.scopes import Scope -big_scope = "" -for i in range({size}): - big_scope += f"foo{{i}}[" -big_scope += "bar" -for _ in range({size}): - big_scope += "]" -""" - elif style == "wide": - setup = f"""\ -from globus_sdk.scopes import Scope -big_scope = "" -for i in range({size}): - big_scope += f"foo{{i}} " -""" - else: - raise NotImplementedError - - timer = timeit.Timer("Scope.parse(big_scope)", setup=setup) - - raw_timings = timer.repeat(repeat=5, number=num_iterations) - best, worst, average, variance = _stats(raw_timings) - if style == "deep": - print(f"{num_iterations} runs on a deep scope, depth={size}") - elif style == "wide": - print(f"{num_iterations} runs on a wide scope, width={size}") - else: - raise NotImplementedError - print(f" best={best} worst={worst} average={average} variance={variance}") - print(f" normalized best={best / num_iterations}") - print() - print("The most informative stat over these timings is the min timing (best).") - print("Normed best is best/iterations.") - print( - "Max timing (worst) and dispersion (variance vis-a-vis average) indicate " - "how consistent the results are, but are not a report of speed." - ) - - -def _stats(timing_data: list[float]) -> tuple[float, float, float, float]: - best = min(timing_data) - worst = max(timing_data) - average = sum(timing_data) / len(timing_data) - variance = sum((x - average) ** 2 for x in timing_data) / len(timing_data) - return best, worst, average, variance - - -def main() -> None: - timeit_test() - - -if __name__ == "__main__": - main() diff --git a/tox.ini b/tox.ini index 1534c8a48..fdc810e0d 100644 --- a/tox.ini +++ b/tox.ini @@ -58,6 +58,12 @@ commands = pytest -n auto tests/non-pytest/lazy-imports/ pytest tests/unit/test_lazy_imports.py +[testenv:benchmark] +deps = + -r requirements/py{py_dot_ver}/test.txt + pytest-benchmark +commands = pytest tests/benchmark/ {posargs} + [testenv:pylint,pylint-{py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}] deps = pylint commands = pylint {posargs:src/} From b42aa5d004fe55c0feebfa93c931dc6177b82ce6 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 9 Jun 2025 16:34:19 -0500 Subject: [PATCH 033/176] Move classproperty to a dedicated private module This was declared in `globus_sdk.utils`. Moving it to `globus_sdk._classproperty` has several benefits: - it is easier to read because the "boundary" around it is at the file-level (previously, it was a chunk of a file) - it makes `utils` smaller and therefore also easier to read - it makes it even more clear that this is an internal component -- historically, some users have pulled in `globus_sdk.utils` and used its components --- src/globus_sdk/_classproperty.py | 71 +++++++++++++++++++++++++++ src/globus_sdk/client.py | 3 +- src/globus_sdk/services/gcs/client.py | 3 +- src/globus_sdk/utils.py | 62 ----------------------- tests/unit/test_classproperty.py | 27 ++++++++++ tests/unit/test_utils.py | 26 ---------- 6 files changed, 102 insertions(+), 90 deletions(-) create mode 100644 src/globus_sdk/_classproperty.py create mode 100644 tests/unit/test_classproperty.py diff --git a/src/globus_sdk/_classproperty.py b/src/globus_sdk/_classproperty.py new file mode 100644 index 000000000..bdb1944d3 --- /dev/null +++ b/src/globus_sdk/_classproperty.py @@ -0,0 +1,71 @@ +""" +WARNING: for internal use only. +Everything in SDK private modules is meant to be internal only, but that +holds for this module **in particular**. + +Usage: + + from globus_sdk._classproperty import classproperty + + class A: + @classproperty + def foo(self_or_cls): ... +""" + +from __future__ import annotations + +import os +import sys +import typing as t + +T = t.TypeVar("T") +R = t.TypeVar("R") + + +def _in_sphinx_build() -> bool: # pragma: no cover + # check if `sphinx-build` was used to invoke + return os.path.basename(sys.argv[0]) in ["sphinx-build", "sphinx-build.exe"] + + +class _classproperty(t.Generic[T, R]): + """ + This is a well-typed Generic Descriptor which can be used to wrap decorated + functions. + + Note that this descriptor will pass an instance (self) if possible, and the + class (cls) only if there is no instance. This is unlike ``classmethod``. + + For more guidance on how this works, see the python3 descriptor guide: + https://docs.python.org/3/howto/descriptor.html#properties + """ + + def __init__(self, func: t.Callable[[type[T] | T], R]) -> None: + self.func = func + + def __get__(self, obj: T | None, cls: type[T]) -> R: + # NOTE: our __get__ here prefers the object over the class when possible + # although well-defined behavior for a descriptor, this contradicts the + # expectation that developers may have from `classmethod` + if obj is None: + return self.func(cls) + return self.func(obj) + + +# if running under sphinx, define this as the stacked classmethod(property(...)) +# decoration, so that proper autodoc generation happens +# this is based on the python3.9 behavior which supported stacking these decorators +# however, that support was pulled in 3.10 and is not going to be reintroduced at +# present +# therefore, this sphinx behavior may not be stable in the long term +if _in_sphinx_build(): # pragma: no cover + + def classproperty(func: t.Callable[[T | type[T]], R]) -> _classproperty[T, R]: + # type ignore this because + # - it doesn't match the return type + # - mypy doesn't understand classmethod(property(...)) on older pythons + return classmethod(property(func)) # type: ignore + +else: + + def classproperty(func: t.Callable[[T | type[T]], R]) -> _classproperty[T, R]: + return _classproperty(func) diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 7caa2ba6a..06af278ea 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -5,6 +5,7 @@ import urllib.parse from globus_sdk import GlobusSDKUsageError, config, exc, utils +from globus_sdk._classproperty import classproperty from globus_sdk._types import ScopeCollectionType from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.paging import PaginatorTable @@ -293,7 +294,7 @@ def app_name(self) -> str | None: def app_name(self, value: str) -> None: self._app_name = self.transport.user_agent = value - @utils.classproperty + @classproperty def resource_server( # pylint: disable=missing-param-doc self_or_cls: BaseClient | type[BaseClient], ) -> str | None: diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 0a15366bb..112e1de8e 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -4,6 +4,7 @@ import uuid from globus_sdk import client, exc, paging, response, scopes, utils +from globus_sdk._classproperty import classproperty from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp @@ -144,7 +145,7 @@ def default_scope_requirements(self) -> list[Scope]: ) ] - @utils.classproperty + @classproperty def resource_server( # pylint: disable=missing-param-doc self_or_cls: client.BaseClient | type[client.BaseClient], ) -> str | None: diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index fe0526006..095665f5b 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -3,18 +3,13 @@ import collections import collections.abc import hashlib -import os import platform -import sys import typing as t import uuid from base64 import b64encode from globus_sdk._types import UUIDLike -T = t.TypeVar("T") -R = t.TypeVar("R") - if t.TYPE_CHECKING: # pylint: disable=unsubscriptable-object PayloadWrapperBase = collections.UserDict[str, t.Any] @@ -231,60 +226,3 @@ def _set_optints(self, **kwargs: t.Any) -> None: """ for k, v in kwargs.items(): self._set_value(k, v, callback=int) - - -def in_sphinx_build() -> bool: # pragma: no cover - # check if `sphinx-build` was used to invoke - return os.path.basename(sys.argv[0]) in ["sphinx-build", "sphinx-build.exe"] - - -class _classproperty(t.Generic[T, R]): - """ - WARNING: for internal use only. - Everything in `globus_sdk.utils` is meant to be internal only, but that holds - for this class **in particular**. - - This is a well-typed Generic Descriptor which can be used to wrap decorated - functions. Usage should be: - - @utils.classproperty - def foo(self_or_cls): ... - - Note that this descriptor will pass an instance (self) if possible, and the - class (cls) only if there is no instance. This is unlike ``classmethod``. - - For more guidance on how this works, see the python3 descriptor guide: - https://docs.python.org/3/howto/descriptor.html#properties - """ - - def __init__(self, func: t.Callable[[type[T]], R]) -> None: - self.func = func - - def __get__(self, obj: t.Any, cls: type[T]) -> R: - # NOTE: our __get__ here prefers the object over the class when possible - # although well-defined behavior for a descriptor, this contradicts the - # expectation that developers may have from `classmethod` - if obj is None: - return self.func(cls) - return self.func(obj) - - -# if running under sphinx, define this as the stacked classmethod(property(...)) -# decoration, so that proper autodoc generation happens -# this is based on the python3.9 behavior which supported stacking these decorators -# however, that support was pulled in 3.10 and is not going to be reintroduced at -# present -# therefore, this sphinx behavior may not be stable in the long term -if in_sphinx_build(): # pragma: no cover - - def classproperty(func: t.Callable[[T], R]) -> _classproperty[T, R]: - # type ignore this because - # - it doesn't match the return type - # - mypy doesn't understand classmethod(property(...)) on older pythons - return classmethod(property(func)) # type: ignore - -else: - - def classproperty(func: t.Callable[[T], R]) -> _classproperty[T, R]: - # type cast to convert instance method to class method - return _classproperty(t.cast(t.Callable[[t.Type[T]], R], func)) diff --git a/tests/unit/test_classproperty.py b/tests/unit/test_classproperty.py new file mode 100644 index 000000000..f399677a8 --- /dev/null +++ b/tests/unit/test_classproperty.py @@ -0,0 +1,27 @@ +from globus_sdk._classproperty import classproperty + + +def test_classproperty_simple(): + class Foo: + x = {"x": 1} + + @classproperty + def y(self_or_cls): + return self_or_cls.x["x"] + + assert Foo.y == 1 + + +def test_classproperty_prefers_instance(): + class Foo: + x = {"x": 1} + + def __init__(self) -> None: + self.x = {"x": 2} + + @classproperty + def y(self_or_cls): + return self_or_cls.x["x"] + + assert Foo.y == 1 + assert Foo().y == 2 diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index c00001bd6..0cb4469bc 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -80,32 +80,6 @@ def test_payload_wrapper_methods(): assert data.data == {"foo": 1, "bar": 2, "x": "hello", "y": "world"} -def test_classproperty_simple(): - class Foo: - x = {"x": 1} - - @utils.classproperty - def y(self_or_cls): - return self_or_cls.x["x"] - - assert Foo.y == 1 - - -def test_classproperty_prefers_instance(): - class Foo: - x = {"x": 1} - - def __init__(self) -> None: - self.x = {"x": 2} - - @utils.classproperty - def y(self_or_cls): - return self_or_cls.x["x"] - - assert Foo.y == 1 - assert Foo().y == 2 - - @pytest.mark.parametrize( "value, expected_result", ( From f4a04809f73a29d0d4d0899c04cc52a2b1477f30 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 9 Jun 2025 23:19:17 -0500 Subject: [PATCH 034/176] Move the MISSING sentinel to a dedicated module `globus_sdk._missing` now provides the sentinel and any related utilities. --- src/globus_sdk/__init__.pyi | 2 +- src/globus_sdk/_missing.py | 58 ++++++++ .../auth/client/confidential_client.py | 35 ++--- .../services/auth/client/service_client.py | 136 ++++++++---------- src/globus_sdk/services/compute/client.py | 2 +- src/globus_sdk/services/compute/data.py | 2 +- src/globus_sdk/services/flows/client.py | 2 +- src/globus_sdk/services/flows/data.py | 3 +- src/globus_sdk/services/gcs/client.py | 2 +- src/globus_sdk/services/gcs/data/_common.py | 2 +- .../services/gcs/data/collection.py | 2 +- src/globus_sdk/services/gcs/data/endpoint.py | 2 +- src/globus_sdk/services/gcs/data/role.py | 2 +- .../services/gcs/data/storage_gateway.py | 2 +- .../services/gcs/data/user_credential.py | 2 +- src/globus_sdk/services/groups/client.py | 2 +- src/globus_sdk/services/groups/data.py | 5 +- src/globus_sdk/services/search/client.py | 2 +- src/globus_sdk/services/search/data.py | 2 +- src/globus_sdk/services/timers/data.py | 3 +- src/globus_sdk/services/transfer/client.py | 2 +- .../services/transfer/data/delete_data.py | 2 +- .../services/transfer/data/transfer_data.py | 2 +- src/globus_sdk/transport/encoders.py | 13 +- src/globus_sdk/utils.py | 50 +------ .../base_client/test_filter_missing.py | 12 +- .../services/flows/test_flow_crud.py | 3 +- .../services/flows/test_flow_validate.py | 3 +- .../functional/services/flows/test_get_run.py | 2 +- .../services/flows/test_list_flows.py | 3 +- .../services/flows/test_list_runs.py | 2 +- .../services/gcs/test_get_collection_list.py | 3 +- .../services/gcs/test_storage_gateways.py | 2 +- .../groups/test_set_group_policies.py | 6 +- .../functional/services/search/test_search.py | 2 +- .../services/timers/test_create_timer.py | 2 +- .../services/transfer/test_operation_mkdir.py | 2 +- .../transfer/test_operation_rename.py | 2 +- tests/unit/helpers/gcs/test_collections.py | 3 +- tests/unit/helpers/test_search.py | 6 +- tests/unit/helpers/test_timer.py | 8 +- tests/unit/helpers/test_transfer.py | 9 +- tests/unit/test_missing_type.py | 14 +- .../unit/transport/test_transport_encoders.py | 3 +- 44 files changed, 210 insertions(+), 214 deletions(-) create mode 100644 src/globus_sdk/_missing.py diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index 5eea2f6b6..f8b283428 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -1,3 +1,4 @@ +from ._missing import MISSING, MissingType from .authorizers import ( AccessTokenAuthorizer, BasicAuthorizer, @@ -125,7 +126,6 @@ from .services.transfer import ( TransferClient, TransferData, ) -from .utils import MISSING, MissingType __version__ = "x.y.z" diff --git a/src/globus_sdk/_missing.py b/src/globus_sdk/_missing.py new file mode 100644 index 000000000..c1fe7b1de --- /dev/null +++ b/src/globus_sdk/_missing.py @@ -0,0 +1,58 @@ +""" +The definition of the MISSING sentinel and its type. + +These are exposed publicly as `globus_sdk.MISSING` and `globus_sdk.MissingType`. +""" + +from __future__ import annotations + +import typing as t + + +class MissingType: + def __init__(self) -> None: + # disable instantiation, but gated to be able to run once + # when this module is imported + if "MISSING" in globals(): + raise TypeError("MissingType should not be instantiated") + + def __bool__(self) -> bool: + return False + + def __copy__(self) -> MissingType: + return self + + def __deepcopy__(self, memo: dict[int, t.Any]) -> MissingType: + return self + + # unpickling a MissingType should always return the "MISSING" sentinel + def __reduce__(self) -> str: + return "MISSING" + + def __repr__(self) -> str: + return "" + + +# a sentinel value for "missing" values which are distinguished from `None` (null) +# this is the default used to indicate that a parameter was not passed, so that +# method calls passing `None` can be distinguished from those which did not pass any +# value +# users should typically not use this value directly, but it is part of the public SDK +# interfaces along with its type for annotation purposes +# +# *new in version 3.30.0* +MISSING = MissingType() + + +@t.overload +def filter_missing(data: dict[str, t.Any]) -> dict[str, t.Any]: ... + + +@t.overload +def filter_missing(data: None) -> None: ... + + +def filter_missing(data: dict[str, t.Any] | None) -> dict[str, t.Any] | None: + if data is None: + return None + return {k: v for k, v in data.items() if v is not MISSING} diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index eb321e55f..5e6672d9c 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -4,6 +4,7 @@ import typing as t from globus_sdk import exc, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import ScopeCollectionType, UUIDLike from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -194,7 +195,7 @@ def oauth2_get_dependent_tokens( token: str, *, refresh_tokens: bool = False, - scope: str | t.Iterable[str] | utils.MissingType = utils.MISSING, + scope: str | t.Iterable[str] | MissingType = MISSING, additional_params: dict[str, t.Any] | None = None, ) -> OAuthDependentTokenResponse: """ @@ -268,7 +269,7 @@ def oauth2_get_dependent_tokens( # back to the user than the OAuth2 spec wording if refresh_tokens: form_data["access_type"] = "offline" - if not isinstance(scope, utils.MissingType): + if not isinstance(scope, MissingType): form_data["scope"] = " ".join(utils.safe_strseq_iter(scope)) if additional_params: form_data.update(additional_params) @@ -332,7 +333,7 @@ def create_child_client( self, name: str, *, - public_client: bool | utils.MissingType = utils.MISSING, + public_client: bool | MissingType = MISSING, client_type: ( t.Literal[ "client_identity", @@ -342,15 +343,15 @@ def create_child_client( "hybrid_confidential_client_resource_server", "resource_server", ] - | utils.MissingType - ) = utils.MISSING, - visibility: t.Literal["public", "private"] | utils.MissingType = utils.MISSING, - redirect_uris: t.Iterable[str] | utils.MissingType = utils.MISSING, - terms_and_conditions: str | utils.MissingType = utils.MISSING, - privacy_policy: str | utils.MissingType = utils.MISSING, - required_idp: UUIDLike | utils.MissingType = utils.MISSING, - preselect_idp: UUIDLike | utils.MissingType = utils.MISSING, - additional_fields: dict[str, t.Any] | utils.MissingType = utils.MISSING, + | MissingType + ) = MISSING, + visibility: t.Literal["public", "private"] | MissingType = MISSING, + redirect_uris: t.Iterable[str] | MissingType = MISSING, + terms_and_conditions: str | MissingType = MISSING, + privacy_policy: str | MissingType = MISSING, + required_idp: UUIDLike | MissingType = MISSING, + preselect_idp: UUIDLike | MissingType = MISSING, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Create a new client. Requires the ``manage_projects`` scope. @@ -435,12 +436,12 @@ def create_child_client( :ref: auth/reference/#create_client """ # Must specify exactly one of public_client or client_type - if public_client is not utils.MISSING and client_type is not utils.MISSING: + if public_client is not MISSING and client_type is not MISSING: raise exc.GlobusSDKUsageError( "AuthClient.create_client does not take both " "'public_client' and 'client_type'. These are mutually exclusive." ) - if public_client is utils.MISSING and client_type is utils.MISSING: + if public_client is MISSING and client_type is MISSING: raise exc.GlobusSDKUsageError( "AuthClient.create_client requires either 'public_client' or " "'client_type'." @@ -454,7 +455,7 @@ def create_child_client( "public_client": public_client, "client_type": client_type, } - if not isinstance(redirect_uris, utils.MissingType): + if not isinstance(redirect_uris, MissingType): body["redirect_uris"] = list(utils.safe_strseq_iter(redirect_uris)) # terms_and_conditions and privacy_policy must both be set or unset @@ -462,14 +463,14 @@ def create_child_client( raise exc.GlobusSDKUsageError( "terms_and_conditions and privacy_policy must both be set or unset" ) - links: dict[str, str | utils.MissingType] = { + links: dict[str, str | MissingType] = { "terms_and_conditions": terms_and_conditions, "privacy_policy": privacy_policy, } if terms_and_conditions or privacy_policy: body["links"] = links - if not isinstance(additional_fields, utils.MissingType): + if not isinstance(additional_fields, MissingType): body.update(additional_fields) return self.post("/v2/api/clients", data={"client": body}) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 44e374f9c..daa17d4cc 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -7,6 +7,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from globus_sdk import client, exc, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.response import GlobusHTTPResponse, IterableResponse @@ -815,15 +816,11 @@ def create_policy( # pylint: disable=missing-param-doc project_id: UUIDLike, display_name: str, description: str, - high_assurance: bool | utils.MissingType = utils.MISSING, - authentication_assurance_timeout: int | utils.MissingType = utils.MISSING, - required_mfa: bool | utils.MissingType = utils.MISSING, - domain_constraints_include: ( - t.Iterable[str] | None | utils.MissingType - ) = utils.MISSING, - domain_constraints_exclude: ( - t.Iterable[str] | None | utils.MissingType - ) = utils.MISSING, + high_assurance: bool | MissingType = MISSING, + authentication_assurance_timeout: int | MissingType = MISSING, + required_mfa: bool | MissingType = MISSING, + domain_constraints_include: t.Iterable[str] | None | MissingType = MISSING, + domain_constraints_exclude: t.Iterable[str] | None | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Create a new Auth policy. Requires the ``manage_projects`` scope. @@ -897,17 +894,13 @@ def update_policy( self, policy_id: UUIDLike, *, - project_id: UUIDLike | utils.MissingType = utils.MISSING, - authentication_assurance_timeout: int | utils.MissingType = utils.MISSING, - required_mfa: bool | utils.MissingType = utils.MISSING, - display_name: str | utils.MissingType = utils.MISSING, - description: str | utils.MissingType = utils.MISSING, - domain_constraints_include: ( - t.Iterable[str] | None | utils.MissingType - ) = utils.MISSING, - domain_constraints_exclude: ( - t.Iterable[str] | None | utils.MissingType - ) = utils.MISSING, + project_id: UUIDLike | MissingType = MISSING, + authentication_assurance_timeout: int | MissingType = MISSING, + required_mfa: bool | MissingType = MISSING, + display_name: str | MissingType = MISSING, + description: str | MissingType = MISSING, + domain_constraints_include: t.Iterable[str] | None | MissingType = MISSING, + domain_constraints_exclude: t.Iterable[str] | None | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Update a policy. Requires the ``manage_projects`` scope. @@ -990,8 +983,8 @@ def delete_policy(self, policy_id: UUIDLike) -> GlobusHTTPResponse: def get_client( self, *, - client_id: UUIDLike | utils.MissingType = utils.MISSING, - fqdn: str | utils.MissingType = utils.MISSING, + client_id: UUIDLike | MissingType = MISSING, + fqdn: str | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Look up a client by ``client_id`` or (exclusive) by ``fqdn``. @@ -1053,18 +1046,18 @@ def get_client( .. extdoclink:: Get Clients :ref: auth/reference/#get_clients """ # noqa: E501 - if client_id is not utils.MISSING and fqdn is not utils.MISSING: + if client_id is not MISSING and fqdn is not MISSING: raise exc.GlobusSDKUsageError( "AuthClient.get_client does not take both " "'client_id' and 'fqdn'. These are mutually exclusive." ) - if client_id is utils.MISSING and fqdn is utils.MISSING: + if client_id is MISSING and fqdn is MISSING: raise exc.GlobusSDKUsageError( "AuthClient.get_client requires either 'client_id' or 'fqdn'." ) - if client_id is not utils.MISSING: + if client_id is not MISSING: return self.get(f"/v2/api/clients/{client_id}") return self.get("/v2/api/clients", query_params={"fqdn": fqdn}) @@ -1141,9 +1134,9 @@ def create_client( name: str, project: UUIDLike, *, - public_client: bool | utils.MissingType = utils.MISSING, + public_client: bool | MissingType = MISSING, client_type: ( - utils.MissingType + MissingType | t.Literal[ "client_identity", "confidential_client", @@ -1152,14 +1145,14 @@ def create_client( "hybrid_confidential_client_resource_server", "resource_server", ] - ) = utils.MISSING, - visibility: utils.MissingType | t.Literal["public", "private"] = utils.MISSING, - redirect_uris: t.Iterable[str] | utils.MissingType = utils.MISSING, - terms_and_conditions: str | utils.MissingType = utils.MISSING, - privacy_policy: str | utils.MissingType = utils.MISSING, - required_idp: UUIDLike | utils.MissingType = utils.MISSING, - preselect_idp: UUIDLike | utils.MissingType = utils.MISSING, - additional_fields: dict[str, t.Any] | utils.MissingType = utils.MISSING, + ) = MISSING, + visibility: MissingType | t.Literal["public", "private"] = MISSING, + redirect_uris: t.Iterable[str] | MissingType = MISSING, + terms_and_conditions: str | MissingType = MISSING, + privacy_policy: str | MissingType = MISSING, + required_idp: UUIDLike | MissingType = MISSING, + preselect_idp: UUIDLike | MissingType = MISSING, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Create a new client. Requires the ``manage_projects`` scope. @@ -1247,12 +1240,12 @@ def create_client( :ref: auth/reference/#create_client """ # Must specify exactly one of public_client or client_type - if public_client is not utils.MISSING and client_type is not utils.MISSING: + if public_client is not MISSING and client_type is not MISSING: raise exc.GlobusSDKUsageError( "AuthClient.create_client does not take both " "'public_client' and 'client_type'. These are mutually exclusive." ) - if public_client is utils.MISSING and client_type is utils.MISSING: + if public_client is MISSING and client_type is MISSING: raise exc.GlobusSDKUsageError( "AuthClient.create_client requires either 'public_client' or " "'client_type'." @@ -1273,14 +1266,14 @@ def create_client( raise exc.GlobusSDKUsageError( "terms_and_conditions and privacy_policy must both be set or unset" ) - links: dict[str, str | utils.MissingType] = { + links: dict[str, str | MissingType] = { "terms_and_conditions": terms_and_conditions, "privacy_policy": privacy_policy, } if terms_and_conditions or privacy_policy: body["links"] = links - if not isinstance(additional_fields, utils.MissingType): + if not isinstance(additional_fields, MissingType): body.update(additional_fields) return self.post("/v2/api/clients", data={"client": body}) @@ -1289,14 +1282,14 @@ def update_client( self, client_id: UUIDLike, *, - name: str | utils.MissingType = utils.MISSING, - visibility: utils.MissingType | t.Literal["public", "private"] = utils.MISSING, - redirect_uris: t.Iterable[str] | utils.MissingType = utils.MISSING, - terms_and_conditions: str | None | utils.MissingType = utils.MISSING, - privacy_policy: str | None | utils.MissingType = utils.MISSING, - required_idp: UUIDLike | None | utils.MissingType = utils.MISSING, - preselect_idp: UUIDLike | None | utils.MissingType = utils.MISSING, - additional_fields: dict[str, t.Any] | utils.MissingType = utils.MISSING, + name: str | MissingType = MISSING, + visibility: MissingType | t.Literal["public", "private"] = MISSING, + redirect_uris: t.Iterable[str] | MissingType = MISSING, + terms_and_conditions: str | None | MissingType = MISSING, + privacy_policy: str | None | MissingType = MISSING, + required_idp: UUIDLike | None | MissingType = MISSING, + preselect_idp: UUIDLike | None | MissingType = MISSING, + additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Update a client. Requires the ``manage_projects`` scope. @@ -1361,17 +1354,14 @@ def update_client( raise exc.GlobusSDKUsageError( "terms_and_conditions and privacy_policy must both be set or unset" ) - links: dict[str, str | None | utils.MissingType] = { + links: dict[str, str | None | MissingType] = { "terms_and_conditions": terms_and_conditions, "privacy_policy": privacy_policy, } - if ( - terms_and_conditions is not utils.MISSING - or privacy_policy is not utils.MISSING - ): + if terms_and_conditions is not MISSING or privacy_policy is not MISSING: body["links"] = links - if not isinstance(additional_fields, utils.MissingType): + if not isinstance(additional_fields, MissingType): body.update(additional_fields) return self.put(f"/v2/api/clients/{client_id}", data={"client": body}) @@ -1575,9 +1565,9 @@ def get_scope(self, scope_id: UUIDLike) -> GlobusHTTPResponse: def get_scopes( self, *, - scope_strings: t.Iterable[str] | str | utils.MissingType = utils.MISSING, - ids: t.Iterable[UUIDLike] | UUIDLike | utils.MissingType = utils.MISSING, - query_params: dict[str, t.Any] | utils.MissingType = utils.MISSING, + scope_strings: t.Iterable[str] | str | MissingType = MISSING, + ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, + query_params: dict[str, t.Any] | MissingType = MISSING, ) -> IterableResponse: """ Look up scopes in projects on which the authenticated user is an admin. @@ -1647,18 +1637,18 @@ def get_scopes( .. extdoclink:: Get Scopes :ref: auth/reference/#get_scopes """ # noqa: E501 - if scope_strings is not utils.MISSING and ids is not utils.MISSING: + if scope_strings is not MISSING and ids is not MISSING: raise exc.GlobusSDKUsageError( "AuthClient.get_scopes does not take both " "'scopes_strings' and 'ids'. These are mutually exclusive." ) - if isinstance(query_params, utils.MissingType): + if isinstance(query_params, MissingType): query_params = {} - if not isinstance(scope_strings, utils.MissingType): + if not isinstance(scope_strings, MissingType): query_params["scope_strings"] = utils.commajoin(scope_strings) - if not isinstance(ids, utils.MissingType): + if not isinstance(ids, MissingType): query_params["ids"] = utils.commajoin(ids) return GetScopesResponse(self.get("/v2/api/scopes", query_params=query_params)) @@ -1670,12 +1660,10 @@ def create_scope( description: str, scope_suffix: str, *, - required_domains: t.Iterable[str] | utils.MissingType = utils.MISSING, - dependent_scopes: ( - t.Iterable[DependentScopeSpec] | utils.MissingType - ) = utils.MISSING, - advertised: bool | utils.MissingType = utils.MISSING, - allows_refresh_token: bool | utils.MissingType = utils.MISSING, + required_domains: t.Iterable[str] | MissingType = MISSING, + dependent_scopes: t.Iterable[DependentScopeSpec] | MissingType = MISSING, + advertised: bool | MissingType = MISSING, + allows_refresh_token: bool | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Create a new scope. Requires the ``manage_projects`` scope. @@ -1738,15 +1726,13 @@ def update_scope( self, scope_id: UUIDLike, *, - name: str | utils.MissingType = utils.MISSING, - description: str | utils.MissingType = utils.MISSING, - scope_suffix: str | utils.MissingType = utils.MISSING, - required_domains: t.Iterable[str] | utils.MissingType = utils.MISSING, - dependent_scopes: ( - t.Iterable[DependentScopeSpec] | utils.MissingType - ) = utils.MISSING, - advertised: bool | utils.MissingType = utils.MISSING, - allows_refresh_token: bool | utils.MissingType = utils.MISSING, + name: str | MissingType = MISSING, + description: str | MissingType = MISSING, + scope_suffix: str | MissingType = MISSING, + required_domains: t.Iterable[str] | MissingType = MISSING, + dependent_scopes: t.Iterable[DependentScopeSpec] | MissingType = MISSING, + advertised: bool | MissingType = MISSING, + allows_refresh_token: bool | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Update a scope. Requires the ``manage_projects`` scope. diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index 027947b6e..ccc32e4bf 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -4,9 +4,9 @@ import typing as t from globus_sdk import GlobusHTTPResponse, client, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike from globus_sdk.scopes import ComputeScopes, Scope -from globus_sdk.utils import MISSING, MissingType from .errors import ComputeAPIError diff --git a/src/globus_sdk/services/compute/data.py b/src/globus_sdk/services/compute/data.py index 2f66712fa..def58e291 100644 --- a/src/globus_sdk/services/compute/data.py +++ b/src/globus_sdk/services/compute/data.py @@ -1,9 +1,9 @@ from __future__ import annotations from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike from globus_sdk.exc import warn_deprecated -from globus_sdk.utils import MISSING, MissingType class ComputeFunctionMetadata(utils.PayloadWrapper): diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 4a2dd0af5..a48f9e145 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -14,6 +14,7 @@ paging, utils, ) +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp @@ -25,7 +26,6 @@ SpecificFlowScopeBuilder, TransferScopes, ) -from globus_sdk.utils import MISSING, MissingType from .data import RunActivityNotificationPolicy from .errors import FlowsAPIError diff --git a/src/globus_sdk/services/flows/data.py b/src/globus_sdk/services/flows/data.py index 8319eee5a..3b297a3e4 100644 --- a/src/globus_sdk/services/flows/data.py +++ b/src/globus_sdk/services/flows/data.py @@ -3,7 +3,8 @@ import logging import typing as t -from globus_sdk.utils import MISSING, MissingType, PayloadWrapper +from globus_sdk._missing import MISSING, MissingType +from globus_sdk.utils import PayloadWrapper log = logging.getLogger(__name__) diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 112e1de8e..0636d1229 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -5,11 +5,11 @@ from globus_sdk import client, exc, paging, response, scopes, utils from globus_sdk._classproperty import classproperty +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import Scope -from globus_sdk.utils import MISSING, MissingType from .connector_table import ConnectorTable from .data import ( diff --git a/src/globus_sdk/services/gcs/data/_common.py b/src/globus_sdk/services/gcs/data/_common.py index 1b4325093..8c732de52 100644 --- a/src/globus_sdk/services/gcs/data/_common.py +++ b/src/globus_sdk/services/gcs/data/_common.py @@ -2,7 +2,7 @@ import typing as t -from globus_sdk.utils import MISSING +from globus_sdk._missing import MISSING VersionTuple = t.Tuple[int, int, int] diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index f81732782..c44022e9a 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -4,8 +4,8 @@ import typing as t from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike -from globus_sdk.utils import MISSING, MissingType from ._common import ( DatatypeCallback, diff --git a/src/globus_sdk/services/gcs/data/endpoint.py b/src/globus_sdk/services/gcs/data/endpoint.py index 07aa6e407..a81f3b47a 100644 --- a/src/globus_sdk/services/gcs/data/endpoint.py +++ b/src/globus_sdk/services/gcs/data/endpoint.py @@ -3,8 +3,8 @@ import typing as t from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk.services.gcs.data._common import DatatypeCallback, ensure_datatype -from globus_sdk.utils import MISSING, MissingType class EndpointDocument(utils.PayloadWrapper): diff --git a/src/globus_sdk/services/gcs/data/role.py b/src/globus_sdk/services/gcs/data/role.py index a947a5967..a0f0344db 100644 --- a/src/globus_sdk/services/gcs/data/role.py +++ b/src/globus_sdk/services/gcs/data/role.py @@ -3,8 +3,8 @@ import typing as t from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike -from globus_sdk.utils import MISSING, MissingType class GCSRoleDocument(utils.PayloadWrapper): diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index 8f0ff02bf..1d379b320 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -4,8 +4,8 @@ import typing as t from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike -from globus_sdk.utils import MISSING, MissingType from ._common import DatatypeCallback, ensure_datatype diff --git a/src/globus_sdk/services/gcs/data/user_credential.py b/src/globus_sdk/services/gcs/data/user_credential.py index 4f2fefe36..da9be19f1 100644 --- a/src/globus_sdk/services/gcs/data/user_credential.py +++ b/src/globus_sdk/services/gcs/data/user_credential.py @@ -3,8 +3,8 @@ import typing as t from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike -from globus_sdk.utils import MISSING, MissingType class UserCredentialDocument(utils.PayloadWrapper): diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index dcbda6c2c..623a12cd6 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -3,9 +3,9 @@ import typing as t from globus_sdk import client, response, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike from globus_sdk.scopes import GroupsScopes, Scope -from globus_sdk.utils import MISSING, MissingType from .data import BatchMembershipActions, GroupPolicies from .errors import GroupsAPIError diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index 27c24b8f9..77bdc1d8b 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -4,6 +4,7 @@ import typing as t from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike T = t.TypeVar("T") @@ -283,9 +284,7 @@ def __init__( group_members_visibility: _GROUP_MEMBER_VISIBILITY_T, join_requests: bool, signup_fields: t.Iterable[_GROUP_REQUIRED_SIGNUP_FIELDS_T], - authentication_assurance_timeout: ( - int | None | utils.MissingType - ) = utils.MISSING, + authentication_assurance_timeout: int | None | MissingType = MISSING, ) -> None: super().__init__() self["is_high_assurance"] = is_high_assurance diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index bb5319ad8..f2068e1ba 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -4,10 +4,10 @@ import typing as t from globus_sdk import client, paging, response, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import Scope, SearchScopes -from globus_sdk.utils import MISSING, MissingType from .data import SearchQuery, SearchScrollQuery from .errors import SearchAPIError diff --git a/src/globus_sdk/services/search/data.py b/src/globus_sdk/services/search/data.py index dc3020998..346337ca3 100644 --- a/src/globus_sdk/services/search/data.py +++ b/src/globus_sdk/services/search/data.py @@ -3,7 +3,7 @@ import typing as t from globus_sdk import exc, utils -from globus_sdk.utils import MISSING, MissingType +from globus_sdk._missing import MISSING, MissingType # workaround for absence of Self type # for the workaround and some background, see: diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index c9f804a72..2ba105009 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -6,10 +6,11 @@ import logging import typing as t +from globus_sdk._missing import MISSING, MissingType from globus_sdk.config import get_service_url from globus_sdk.exc import warn_deprecated from globus_sdk.services.transfer import TransferData -from globus_sdk.utils import MISSING, MissingType, PayloadWrapper, slash_join +from globus_sdk.utils import PayloadWrapper, slash_join log = logging.getLogger(__name__) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 35533e08b..446a33fae 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -6,9 +6,9 @@ import uuid from globus_sdk import _guards, client, exc, paging, response, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import DateLike, IntLike, UUIDLike from globus_sdk.scopes import GCSCollectionScopeBuilder, Scope, TransferScopes -from globus_sdk.utils import MISSING, MissingType from .data import DeleteData, TransferData from .errors import TransferAPIError diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index 94d9e05b8..ec4972dd5 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -5,8 +5,8 @@ import typing as t from globus_sdk import exc, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike -from globus_sdk.utils import MISSING, MissingType if t.TYPE_CHECKING: import globus_sdk diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index 8f28fddce..cb31f6ca0 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -5,8 +5,8 @@ import typing as t from globus_sdk import exc, utils +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike -from globus_sdk.utils import MISSING, MissingType if t.TYPE_CHECKING: import globus_sdk diff --git a/src/globus_sdk/transport/encoders.py b/src/globus_sdk/transport/encoders.py index 95b331458..359564136 100644 --- a/src/globus_sdk/transport/encoders.py +++ b/src/globus_sdk/transport/encoders.py @@ -7,6 +7,7 @@ import requests from globus_sdk import utils +from globus_sdk._missing import MISSING, filter_missing class RequestEncoder: @@ -66,9 +67,7 @@ def _prepare_params( """ if params is None: return None - return utils.filter_missing( - {k: self._format_primitive(v) for k, v in params.items()} - ) + return filter_missing({k: self._format_primitive(v) for k, v in params.items()}) def _prepare_headers( self, headers: dict[str, t.Any] | None @@ -80,7 +79,7 @@ def _prepare_headers( """ if headers is None: return None - return utils.filter_missing( + return filter_missing( {k: self._format_primitive(v) for k, v in headers.items()} ) @@ -94,11 +93,9 @@ def _prepare_data(self, data: t.Any) -> t.Any: Otherwise, it is returned as-is. """ if isinstance(data, (dict, utils.PayloadWrapper)): - return utils.filter_missing( - {k: self._prepare_data(v) for k, v in data.items()} - ) + return filter_missing({k: self._prepare_data(v) for k, v in data.items()}) elif isinstance(data, (list, tuple)): - return [self._prepare_data(x) for x in data if x is not utils.MISSING] + return [self._prepare_data(x) for x in data if x is not MISSING] else: return self._format_primitive(data) diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index 095665f5b..3a5f3b789 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -8,6 +8,7 @@ import uuid from base64 import b64encode +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike if t.TYPE_CHECKING: @@ -17,55 +18,6 @@ PayloadWrapperBase = collections.UserDict -class MissingType: - def __init__(self) -> None: - # disable instantiation, but gated to be able to run once - # when this module is imported - if "MISSING" in globals(): - raise TypeError("MissingType should not be instantiated") - - def __bool__(self) -> bool: - return False - - def __copy__(self) -> MissingType: - return self - - def __deepcopy__(self, memo: dict[int, t.Any]) -> MissingType: - return self - - # unpickling a MissingType should always return the "MISSING" sentinel - def __reduce__(self) -> str: - return "MISSING" - - def __repr__(self) -> str: - return "" - - -# a sentinel value for "missing" values which are distinguished from `None` (null) -# this is the default used to indicate that a parameter was not passed, so that -# method calls passing `None` can be distinguished from those which did not pass any -# value -# users should typically not use this value directly, but it is part of the public SDK -# interfaces along with its type for annotation purposes -# -# *new in version 3.30.0* -MISSING = MissingType() - - -@t.overload -def filter_missing(data: dict[str, t.Any]) -> dict[str, t.Any]: ... - - -@t.overload -def filter_missing(data: None) -> None: ... - - -def filter_missing(data: dict[str, t.Any] | None) -> dict[str, t.Any] | None: - if data is None: - return None - return {k: v for k, v in data.items() if v is not MISSING} - - def sha256_string(s: str) -> str: return hashlib.sha256(s.encode("utf-8")).hexdigest() diff --git a/tests/functional/base_client/test_filter_missing.py b/tests/functional/base_client/test_filter_missing.py index a90b828d6..127994ef9 100644 --- a/tests/functional/base_client/test_filter_missing.py +++ b/tests/functional/base_client/test_filter_missing.py @@ -3,7 +3,7 @@ import pytest -from globus_sdk import utils +from globus_sdk import MISSING from globus_sdk._testing import RegisteredResponse, get_last_request, load_response @@ -25,14 +25,14 @@ def setup_mock_responses(): def test_query_params_can_filter_missing(client): - res = client.get("/bar", query_params={"foo": "bar", "baz": utils.MISSING}) + res = client.get("/bar", query_params={"foo": "bar", "baz": MISSING}) assert res.http_status == 200 req = get_last_request() assert req.params == {"foo": "bar"} def test_headers_can_filter_missing(client): - res = client.get("/bar", headers={"foo": "bar", "baz": utils.MISSING}) + res = client.get("/bar", headers={"foo": "bar", "baz": MISSING}) assert res.http_status == 200 req = get_last_request() assert req.headers["foo"] == "bar" @@ -40,7 +40,7 @@ def test_headers_can_filter_missing(client): def test_json_body_can_filter_missing(client): - res = client.post("/bar", data={"foo": "bar", "baz": utils.MISSING}) + res = client.post("/bar", data={"foo": "bar", "baz": MISSING}) assert res.http_status == 200 req = get_last_request() sent = json.loads(req.body) @@ -48,9 +48,7 @@ def test_json_body_can_filter_missing(client): def test_form_body_can_filter_missing(client): - res = client.post( - "/bar", data={"foo": "bar", "baz": utils.MISSING}, encoding="form" - ) + res = client.post("/bar", data={"foo": "bar", "baz": MISSING}, encoding="form") assert res.http_status == 200 req = get_last_request() sent = urllib.parse.parse_qs(req.body) diff --git a/tests/functional/services/flows/test_flow_crud.py b/tests/functional/services/flows/test_flow_crud.py index 7670c9820..750b8cf82 100644 --- a/tests/functional/services/flows/test_flow_crud.py +++ b/tests/functional/services/flows/test_flow_crud.py @@ -3,10 +3,9 @@ import pytest from responses import matchers -from globus_sdk import FlowsAPIError +from globus_sdk import MISSING, FlowsAPIError from globus_sdk._testing import get_last_request, load_response from globus_sdk._testing.models import RegisteredResponse -from globus_sdk.utils import MISSING @pytest.mark.parametrize("subscription_id", [MISSING, None, "dummy_subscription_id"]) diff --git a/tests/functional/services/flows/test_flow_validate.py b/tests/functional/services/flows/test_flow_validate.py index 52f8172c0..668de99e2 100644 --- a/tests/functional/services/flows/test_flow_validate.py +++ b/tests/functional/services/flows/test_flow_validate.py @@ -2,9 +2,8 @@ import pytest -from globus_sdk import FlowsAPIError +from globus_sdk import MISSING, FlowsAPIError from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING @pytest.mark.parametrize("input_schema", [MISSING, {}]) diff --git a/tests/functional/services/flows/test_get_run.py b/tests/functional/services/flows/test_get_run.py index 6324d540c..7944c572f 100644 --- a/tests/functional/services/flows/test_get_run.py +++ b/tests/functional/services/flows/test_get_run.py @@ -1,7 +1,7 @@ import pytest +from globus_sdk import MISSING from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING @pytest.mark.parametrize("include_flow_description", (MISSING, False, True)) diff --git a/tests/functional/services/flows/test_list_flows.py b/tests/functional/services/flows/test_list_flows.py index 9b1c4bb39..6cffff532 100644 --- a/tests/functional/services/flows/test_list_flows.py +++ b/tests/functional/services/flows/test_list_flows.py @@ -2,9 +2,8 @@ import pytest -from globus_sdk import GlobusSDKUsageError, RemovedInV4Warning +from globus_sdk import MISSING, GlobusSDKUsageError, RemovedInV4Warning from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING @pytest.mark.parametrize("filter_fulltext", [MISSING, "foo"]) diff --git a/tests/functional/services/flows/test_list_runs.py b/tests/functional/services/flows/test_list_runs.py index 8ecc7b51d..9edc08a32 100644 --- a/tests/functional/services/flows/test_list_runs.py +++ b/tests/functional/services/flows/test_list_runs.py @@ -3,8 +3,8 @@ import pytest +from globus_sdk import MISSING from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING def test_list_runs_simple(flows_client): diff --git a/tests/functional/services/gcs/test_get_collection_list.py b/tests/functional/services/gcs/test_get_collection_list.py index b13aa0358..95ec90a3f 100644 --- a/tests/functional/services/gcs/test_get_collection_list.py +++ b/tests/functional/services/gcs/test_get_collection_list.py @@ -1,8 +1,7 @@ import pytest -from globus_sdk import GCSAPIError +from globus_sdk import MISSING, GCSAPIError from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING def test_get_collection_list(client): diff --git a/tests/functional/services/gcs/test_storage_gateways.py b/tests/functional/services/gcs/test_storage_gateways.py index 16fcb8d7c..747d08cbe 100644 --- a/tests/functional/services/gcs/test_storage_gateways.py +++ b/tests/functional/services/gcs/test_storage_gateways.py @@ -3,8 +3,8 @@ import pytest import globus_sdk +from globus_sdk import MISSING from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING @pytest.mark.parametrize( diff --git a/tests/functional/services/groups/test_set_group_policies.py b/tests/functional/services/groups/test_set_group_policies.py index 7406967a2..7c25b822c 100644 --- a/tests/functional/services/groups/test_set_group_policies.py +++ b/tests/functional/services/groups/test_set_group_policies.py @@ -3,11 +3,11 @@ import pytest from globus_sdk import ( + MISSING, GroupMemberVisibility, GroupPolicies, GroupRequiredSignupFields, GroupVisibility, - utils, ) from globus_sdk._testing import get_last_request, load_response @@ -87,7 +87,7 @@ def test_set_group_policies( GroupMemberVisibility.managers, ["address1"], ["address1"], - utils.MISSING, + MISSING, ), ( "private", @@ -147,7 +147,7 @@ def test_set_group_policies_explicit_payload( # check the authentication_assurance_timeout # it should be omitted if it's MISSING - if auth_timeout is utils.MISSING: + if auth_timeout is MISSING: assert "authentication_assurance_timeout" not in req_body else: assert req_body["authentication_assurance_timeout"] == auth_timeout diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index 9bc1aab67..218f714a4 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -6,8 +6,8 @@ import responses import globus_sdk +from globus_sdk._missing import filter_missing from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import filter_missing from tests.common import register_api_route_fixture_file diff --git a/tests/functional/services/timers/test_create_timer.py b/tests/functional/services/timers/test_create_timer.py index 4689b63a3..a62909aaa 100644 --- a/tests/functional/services/timers/test_create_timer.py +++ b/tests/functional/services/timers/test_create_timer.py @@ -1,8 +1,8 @@ import json import globus_sdk +from globus_sdk._missing import filter_missing from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import filter_missing def test_dummy_timer_creation(client): diff --git a/tests/functional/services/transfer/test_operation_mkdir.py b/tests/functional/services/transfer/test_operation_mkdir.py index 39cbbbe13..844dadeca 100644 --- a/tests/functional/services/transfer/test_operation_mkdir.py +++ b/tests/functional/services/transfer/test_operation_mkdir.py @@ -3,8 +3,8 @@ import pytest +from globus_sdk import MISSING from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING _OMIT = object() diff --git a/tests/functional/services/transfer/test_operation_rename.py b/tests/functional/services/transfer/test_operation_rename.py index 7610c3a58..981593320 100644 --- a/tests/functional/services/transfer/test_operation_rename.py +++ b/tests/functional/services/transfer/test_operation_rename.py @@ -3,8 +3,8 @@ import pytest +from globus_sdk import MISSING from globus_sdk._testing import get_last_request, load_response -from globus_sdk.utils import MISSING _OMIT = object() diff --git a/tests/unit/helpers/gcs/test_collections.py b/tests/unit/helpers/gcs/test_collections.py index 1a8b41988..70f1b2b05 100644 --- a/tests/unit/helpers/gcs/test_collections.py +++ b/tests/unit/helpers/gcs/test_collections.py @@ -12,8 +12,9 @@ POSIXCollectionPolicies, POSIXStagingCollectionPolicies, ) +from globus_sdk._missing import MISSING, MissingType, filter_missing from globus_sdk.transport import JSONRequestEncoder -from globus_sdk.utils import MISSING, MissingType, UUIDLike, filter_missing +from globus_sdk.utils import UUIDLike STUB_SG_ID = uuid.uuid1() # storage gateway STUB_MC_ID = uuid.uuid1() # mapped collection diff --git a/tests/unit/helpers/test_search.py b/tests/unit/helpers/test_search.py index f37c9178f..52b150654 100644 --- a/tests/unit/helpers/test_search.py +++ b/tests/unit/helpers/test_search.py @@ -4,8 +4,8 @@ import pytest -from globus_sdk import RemovedInV4Warning, SearchQuery, SearchQueryV1, utils -from globus_sdk.utils import filter_missing +from globus_sdk import MISSING, RemovedInV4Warning, SearchQuery, SearchQueryV1 +from globus_sdk._missing import filter_missing def test_init_legacy(): @@ -47,7 +47,7 @@ def test_init_v1(): # ensure key attributes initialize to empty lists for attribute in ["facets", "filters", "post_facet_filters", "sort", "boosts"]: - assert query[attribute] == utils.MISSING + assert query[attribute] == MISSING # init with supported fields params = {"q": "foo", "limit": 10, "offset": 0, "advanced": False} diff --git a/tests/unit/helpers/test_timer.py b/tests/unit/helpers/test_timer.py index e67310fca..b6bff6c1b 100644 --- a/tests/unit/helpers/test_timer.py +++ b/tests/unit/helpers/test_timer.py @@ -9,8 +9,8 @@ TransferData, TransferTimer, exc, - utils, ) +from globus_sdk._missing import filter_missing from tests.common import GO_EP1_ID, GO_EP2_ID @@ -81,7 +81,7 @@ def test_once_timer_schedule_formats_datetime(input_time, expected): def test_recurring_timer_schedule_interval_only(): schedule = RecurringTimerSchedule(interval_seconds=600) - assert utils.filter_missing(schedule) == { + assert filter_missing(schedule) == { "type": "recurring", "interval_seconds": 600, } @@ -99,7 +99,7 @@ def test_recurring_timer_schedule_interval_only(): ) def test_recurring_timer_schedule_formats_start(input_time, expected): schedule = RecurringTimerSchedule(interval_seconds=600, start=input_time) - assert utils.filter_missing(schedule) == { + assert filter_missing(schedule) == { "type": "recurring", "interval_seconds": 600, "start": expected, @@ -112,7 +112,7 @@ def test_recurring_timer_schedule_formats_datetime_for_end(): schedule = RecurringTimerSchedule( interval_seconds=600, end={"condition": "time", "datetime": end_time} ) - assert utils.filter_missing(schedule) == { + assert filter_missing(schedule) == { "type": "recurring", "interval_seconds": 600, "end": {"condition": "time", "datetime": "2023-10-27T05:38:49+00:00"}, diff --git a/tests/unit/helpers/test_transfer.py b/tests/unit/helpers/test_transfer.py index 7efb1d8da..9d23aef2b 100644 --- a/tests/unit/helpers/test_transfer.py +++ b/tests/unit/helpers/test_transfer.py @@ -1,9 +1,14 @@ import pytest -from globus_sdk import DeleteData, GlobusSDKUsageError, TransferClient, TransferData +from globus_sdk import ( + MISSING, + DeleteData, + GlobusSDKUsageError, + TransferClient, + TransferData, +) from globus_sdk._testing import load_response from globus_sdk.services.transfer.client import _format_filter -from globus_sdk.utils import MISSING from tests.common import GO_EP1_ID, GO_EP2_ID diff --git a/tests/unit/test_missing_type.py b/tests/unit/test_missing_type.py index a0db730a1..d10599a99 100644 --- a/tests/unit/test_missing_type.py +++ b/tests/unit/test_missing_type.py @@ -3,26 +3,26 @@ import pytest -from globus_sdk import utils +from globus_sdk._missing import MISSING, MissingType def test_missing_type_cannot_be_instantiated(): with pytest.raises(TypeError, match="MissingType should not be instantiated"): - utils.MissingType() + MissingType() def test_missing_sentinel_bools_as_false(): - assert bool(utils.MISSING) is False + assert bool(MISSING) is False def test_str_of_missing(): - assert str(utils.MISSING) == "" + assert str(MISSING) == "" def test_copy_of_missing_is_self(): - assert copy.copy(utils.MISSING) is utils.MISSING - assert copy.deepcopy(utils.MISSING) is utils.MISSING + assert copy.copy(MISSING) is MISSING + assert copy.deepcopy(MISSING) is MISSING def test_pickle_of_missing_is_self(): - assert pickle.loads(pickle.dumps(utils.MISSING)) is utils.MISSING + assert pickle.loads(pickle.dumps(MISSING)) is MISSING diff --git a/tests/unit/transport/test_transport_encoders.py b/tests/unit/transport/test_transport_encoders.py index 0cc373e8e..a06613f01 100644 --- a/tests/unit/transport/test_transport_encoders.py +++ b/tests/unit/transport/test_transport_encoders.py @@ -2,8 +2,9 @@ import pytest +from globus_sdk import MISSING from globus_sdk.transport import FormRequestEncoder, JSONRequestEncoder, RequestEncoder -from globus_sdk.utils import MISSING, PayloadWrapper +from globus_sdk.utils import PayloadWrapper @pytest.mark.parametrize("data", ("foo", b"bar")) From 19e26ca59c1b73fa8c01010632d1fd45fe61f93d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 10 Jun 2025 10:26:36 -0500 Subject: [PATCH 035/176] Move a base64 helper from `utils` This helper has only one usage site. Re-home it there. Because the tests of the helper confirmed the resulting values, we'll do the same now in the tests of the higher level utility that uses it. --- src/globus_sdk/authorizers/basic.py | 9 ++++++--- src/globus_sdk/utils.py | 5 ----- tests/unit/authorizers/test_basic_authorizer.py | 11 +++++++++-- tests/unit/test_utils.py | 14 -------------- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/globus_sdk/authorizers/basic.py b/src/globus_sdk/authorizers/basic.py index 6960cc8c8..8531e41ee 100644 --- a/src/globus_sdk/authorizers/basic.py +++ b/src/globus_sdk/authorizers/basic.py @@ -1,7 +1,6 @@ +import base64 import logging -from globus_sdk import utils - from .base import StaticGlobusAuthorizer log = logging.getLogger(__name__) @@ -27,4 +26,8 @@ def __init__(self, username: str, password: str) -> None: self.password = password to_b64 = f"{username}:{password}" - self.header_val = f"Basic {utils.b64str(to_b64)}" + self.header_val = f"Basic {_b64str(to_b64)}" + + +def _b64str(s: str) -> str: + return base64.b64encode(s.encode("utf-8")).decode("utf-8") diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index 3a5f3b789..22c553c2f 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -6,7 +6,6 @@ import platform import typing as t import uuid -from base64 import b64encode from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike @@ -22,10 +21,6 @@ def sha256_string(s: str) -> str: return hashlib.sha256(s.encode("utf-8")).hexdigest() -def b64str(s: str) -> str: - return b64encode(s.encode("utf-8")).decode("utf-8") - - def get_nice_hostname() -> str | None: """ Get the current hostname, with the following added behavior: diff --git a/tests/unit/authorizers/test_basic_authorizer.py b/tests/unit/authorizers/test_basic_authorizer.py index fbf2c9383..31ae3104e 100644 --- a/tests/unit/authorizers/test_basic_authorizer.py +++ b/tests/unit/authorizers/test_basic_authorizer.py @@ -19,6 +19,7 @@ def test_get_authorization_header(authorizer): """ header_val = authorizer.get_authorization_header() assert header_val[:6] == "Basic " + assert header_val[6:] == "dGVzdFVzZXI6UEFTU1dPUkQ=" decoded = base64.b64decode(header_val[6:].encode("utf-8")).decode("utf-8") assert decoded == f"{USERNAME}:{PASSWORD}" @@ -31,9 +32,14 @@ def test_handle_missing_authorization(authorizer): @pytest.mark.parametrize( - "username, password", [("user", "テスト"), ("дум", "pass"), ("テスト", "дум")] + "username, password, encoded_value", + [ + ("user", "テスト", "dXNlcjrjg4bjgrnjg4g="), + ("дум", "pass", "0LTRg9C8OnBhc3M="), + ("テスト", "дум", "44OG44K544OIOtC00YPQvA=="), + ], ) -def test_unicode_handling(username, password): +def test_unicode_handling(username, password, encoded_value): """ With a unicode string for the password, set and verify the Authorization header. @@ -42,5 +48,6 @@ def test_unicode_handling(username, password): header_val = authorizer.get_authorization_header() assert header_val[:6] == "Basic " + assert header_val[6:] == encoded_value decoded = base64.b64decode(header_val[6:].encode("utf-8")).decode("utf-8") assert decoded == f"{username}:{password}" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 0cb4469bc..61cca022e 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,20 +5,6 @@ from globus_sdk import utils -def test_b64str_non_ascii(): - test_string = "ⓤⓢⓔⓡⓝⓐⓜⓔ" - expected_b64 = "4pOk4pOi4pOU4pOh4pOd4pOQ4pOc4pOU" - - assert utils.b64str(test_string) == expected_b64 - - -def test_b64str_ascii(): - test_string = "username" - expected_b64 = "dXNlcm5hbWU=" - - assert utils.b64str(test_string) == expected_b64 - - def test_sha256string(): test_string = "foo" expected_sha = "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" From 49b7687215aa23991fcd9c44af4f5e16e9786432 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 10 Jun 2025 13:20:09 -0500 Subject: [PATCH 036/176] Convert "PayloadWrapper" type to "Payload" - Move it from `utils` to a dedicated `_payload` module - Update Payload to inherit from dict - Update types to avoid listing it explicitly where no longer necessary (since it's a dict) - Update all imports of the Payload type - Define an AbstractPayload type to handle metaclass conflict with dict when an abstract type is wanted - Remove public doc of Payload --- docs/core/utils.rst | 14 +-- src/globus_sdk/_payload.py | 117 ++++++++++++++++++ src/globus_sdk/client.py | 2 +- .../services/auth/client/base_login_client.py | 10 +- src/globus_sdk/services/auth/data.py | 4 +- src/globus_sdk/services/compute/data.py | 6 +- src/globus_sdk/services/flows/data.py | 4 +- .../services/gcs/data/collection.py | 7 +- src/globus_sdk/services/gcs/data/endpoint.py | 3 +- src/globus_sdk/services/gcs/data/role.py | 4 +- .../services/gcs/data/storage_gateway.py | 6 +- .../services/gcs/data/user_credential.py | 4 +- src/globus_sdk/services/groups/data.py | 5 +- src/globus_sdk/services/search/data.py | 7 +- src/globus_sdk/services/timers/data.py | 11 +- .../services/transfer/data/__init__.py | 5 +- .../services/transfer/data/delete_data.py | 5 +- .../services/transfer/data/transfer_data.py | 5 +- src/globus_sdk/transport/encoders.py | 7 +- src/globus_sdk/transport/requests.py | 10 +- src/globus_sdk/utils.py | 92 +------------- tests/unit/helpers/gcs/test_collections.py | 4 +- tests/unit/helpers/test_timer.py | 2 +- tests/unit/test_payload.py | 73 +++++++++++ tests/unit/test_utils.py | 21 ---- .../unit/transport/test_transport_encoders.py | 16 +-- 26 files changed, 258 insertions(+), 186 deletions(-) create mode 100644 src/globus_sdk/_payload.py create mode 100644 tests/unit/test_payload.py diff --git a/docs/core/utils.rst b/docs/core/utils.rst index 796359b45..a83825745 100644 --- a/docs/core/utils.rst +++ b/docs/core/utils.rst @@ -3,24 +3,14 @@ Utilities .. warning:: - The components in this module are *not* intended for outside use, but are - internal to the Globus SDK. + These components are *not* intended for outside use, but are internal to + the Globus SDK. They may change in backwards-incompatible ways in minor or patch releases of the SDK. This documentation is included here for completeness. -PayloadWrapper --------------- - -The ``PayloadWrapper`` class is used as a base class for all Globus SDK -payload datatypes to provide nicer interfaces for payload construction. - -The objects are a type of ``UserDict`` with no special methods. - -.. autoclass:: globus_sdk.utils.PayloadWrapper - MissingType and MISSING ----------------------- diff --git a/src/globus_sdk/_payload.py b/src/globus_sdk/_payload.py new file mode 100644 index 000000000..917bed1a7 --- /dev/null +++ b/src/globus_sdk/_payload.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import abc +import typing as t + +from globus_sdk._missing import MISSING, MissingType +from globus_sdk.utils import safe_strseq_iter + +if t.TYPE_CHECKING: + # pylint: disable=unsubscriptable-object + _PayloadBaseDict = dict[str, t.Any] +else: + _PayloadBaseDict = dict + + +class Payload(_PayloadBaseDict): + """ + A class for defining helper objects which wrap some kind of "payload" dict. + Typical for helper objects which formulate a request payload. + + Payload types inheriting from this class can be passed directly to the client + ``post()``, ``put()``, and ``patch()`` methods. These methods will + recognize a ``PayloadBase`` and apply conversions for serialization with + the requested encoder (e.g. as a JSON request body). + """ + + # + # internal helpers for setting non-null values + # + + def _set_value( + self, + key: str, + val: t.Any, + callback: t.Callable[[t.Any], t.Any] | None = None, + ) -> None: + """ + Internal helper for setting an omittable value on the payload. + + If the value is non-None, it will be set and the callback (if provided) will be + invoked on it. + Otherwise, it will be ignored and the callback will not be invoked. + + :param key: The key to set. + :param val: The value to set. + :param callback: An optional callback to apply to the value immediately + before it is set. + """ + if val is not None and val is not MISSING: + self[key] = callback(val) if callback else val + + def _set_optstrs(self, **kwargs: t.Any) -> None: + """ + Convenience function for setting a collection of omittable string values. + + Values are converted to strings prior to assignment. + """ + for k, v in kwargs.items(): + self._set_value(k, v, callback=str) + + def _set_optstrlists( + self, **kwargs: t.Iterable[t.Any] | None | MissingType + ) -> None: + """ + Convenience function for setting a collection of omittable string list values. + + Values are converted to lists of strings prior to assignment. + """ + for k, v in kwargs.items(): + self._set_value(k, v, callback=lambda x: list(safe_strseq_iter(x))) + + def _set_optbools(self, **kwargs: bool | None | MissingType) -> None: + """ + Convenience function for setting a collection of omittable bool values. + + Values are converted to bools prior to assignment. + """ + for k, v in kwargs.items(): + self._set_value(k, v, callback=bool) + + def _set_optints(self, **kwargs: t.Any) -> None: + """ + Convenience function for setting a collection of omittable int values. + + Values are converted to ints prior to assignment. + """ + for k, v in kwargs.items(): + self._set_value(k, v, callback=int) + + +class AbstractPayload(Payload, abc.ABC): + """ + An abstract class which is a Payload. + + This is a shim which is needed because we have a metaclass conflict between + dict:type and ABC:ABCMeta. + + Setting the metaclass helps type checkers understand that such classes are + abstract. + """ + + # explicitly define `__new__` in order to check for abstract methods which + # were not redefined + def __new__(cls, *args: t.Any, **kwargs: t.Any) -> t.Self: + obj = super().__new__(cls, *args, **kwargs) + abstractmethods: frozenset[str] = ( + obj.__abstractmethods__ # type: ignore[attr-defined] + ) + if abstractmethods: + s = "" if len(abstractmethods) == 1 else "s" + methodnames = ", ".join(f"'{f}'" for f in abstractmethods) + # this error very closely imitates the errors produced by ABCMeta + raise TypeError( + f"Can't instantiate abstract class {cls.__name__} without " + f"an implementation for abstract method{s} {methodnames}" + ) + return obj diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 06af278ea..9fd0918c6 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) -_DataParamType = t.Union[None, str, bytes, t.Dict[str, t.Any], utils.PayloadWrapper] +_DataParamType: t.TypeAlias = t.Union[None, str, bytes, t.Dict[str, t.Any]] class BaseClient: diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 84f4a1981..3756a4156 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -344,13 +344,13 @@ def oauth2_revoke_token( @t.overload def oauth2_token( self, - form_data: dict[str, t.Any] | utils.PayloadWrapper, + form_data: dict[str, t.Any], ) -> OAuthTokenResponse: ... @t.overload def oauth2_token( self, - form_data: dict[str, t.Any] | utils.PayloadWrapper, + form_data: dict[str, t.Any], *, body_params: dict[str, t.Any] | None, ) -> OAuthTokenResponse: ... @@ -358,7 +358,7 @@ def oauth2_token( @t.overload def oauth2_token( self, - form_data: dict[str, t.Any] | utils.PayloadWrapper, + form_data: dict[str, t.Any], *, response_class: type[RT], ) -> RT: ... @@ -366,7 +366,7 @@ def oauth2_token( @t.overload def oauth2_token( self, - form_data: dict[str, t.Any] | utils.PayloadWrapper, + form_data: dict[str, t.Any], *, body_params: dict[str, t.Any] | None, response_class: type[RT], @@ -374,7 +374,7 @@ def oauth2_token( def oauth2_token( self, - form_data: dict[str, t.Any] | utils.PayloadWrapper, + form_data: dict[str, t.Any], *, body_params: dict[str, t.Any] | None = None, response_class: type[OAuthTokenResponse] | type[RT] = OAuthTokenResponse, diff --git a/src/globus_sdk/services/auth/data.py b/src/globus_sdk/services/auth/data.py index c560073ca..af4057d56 100644 --- a/src/globus_sdk/services/auth/data.py +++ b/src/globus_sdk/services/auth/data.py @@ -1,8 +1,8 @@ -from globus_sdk import utils +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike -class DependentScopeSpec(utils.PayloadWrapper): +class DependentScopeSpec(Payload): """ Utility class for creating dependent scope values as parameters to :meth:`AuthClient.create_scope ` diff --git a/src/globus_sdk/services/compute/data.py b/src/globus_sdk/services/compute/data.py index def58e291..fedfac2e6 100644 --- a/src/globus_sdk/services/compute/data.py +++ b/src/globus_sdk/services/compute/data.py @@ -1,12 +1,12 @@ from __future__ import annotations -from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike from globus_sdk.exc import warn_deprecated -class ComputeFunctionMetadata(utils.PayloadWrapper): +class ComputeFunctionMetadata(Payload): """ .. warning:: @@ -30,7 +30,7 @@ def __init__( self["sdk_version"] = sdk_version -class ComputeFunctionDocument(utils.PayloadWrapper): +class ComputeFunctionDocument(Payload): """ .. warning:: diff --git a/src/globus_sdk/services/flows/data.py b/src/globus_sdk/services/flows/data.py index 3b297a3e4..9a44c0917 100644 --- a/src/globus_sdk/services/flows/data.py +++ b/src/globus_sdk/services/flows/data.py @@ -4,12 +4,12 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk.utils import PayloadWrapper +from globus_sdk._payload import Payload log = logging.getLogger(__name__) -class RunActivityNotificationPolicy(PayloadWrapper): +class RunActivityNotificationPolicy(Payload): """ A notification policy for a run, determining when emails will be sent. diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index c44022e9a..2ac4866e4 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -5,6 +5,7 @@ from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import AbstractPayload from globus_sdk._types import UUIDLike from ._common import ( @@ -58,7 +59,9 @@ def _user_message_length_callback( return None -class CollectionDocument(utils.PayloadWrapper, abc.ABC): +# Declare a metaclass of ABCMeta even though inheriting from `Payload` renders it +# inert. This will let type checkers understand that this class is abstract. +class CollectionDocument(AbstractPayload): """ This is the base class for :class:`~.MappedCollectionDocument` and :class:`~.GuestCollectionDocument`. @@ -496,7 +499,7 @@ def __init__( ensure_datatype(self) -class CollectionPolicies(utils.PayloadWrapper, abc.ABC): +class CollectionPolicies(AbstractPayload): """ This is the abstract base type for Collection Policies documents to use as the ``policies`` parameter when creating a MappedCollectionDocument. diff --git a/src/globus_sdk/services/gcs/data/endpoint.py b/src/globus_sdk/services/gcs/data/endpoint.py index a81f3b47a..da2f55fe5 100644 --- a/src/globus_sdk/services/gcs/data/endpoint.py +++ b/src/globus_sdk/services/gcs/data/endpoint.py @@ -4,10 +4,11 @@ from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk.services.gcs.data._common import DatatypeCallback, ensure_datatype -class EndpointDocument(utils.PayloadWrapper): +class EndpointDocument(Payload): r""" :param data_type: Explicitly set the ``DATA_TYPE`` value for this endpoint document. diff --git a/src/globus_sdk/services/gcs/data/role.py b/src/globus_sdk/services/gcs/data/role.py index a0f0344db..2371f8020 100644 --- a/src/globus_sdk/services/gcs/data/role.py +++ b/src/globus_sdk/services/gcs/data/role.py @@ -2,12 +2,12 @@ import typing as t -from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike -class GCSRoleDocument(utils.PayloadWrapper): +class GCSRoleDocument(Payload): """ Convenience class for constructing a Role document to use as the `data` parameter to `create_role` diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index 1d379b320..c8344fb7e 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -3,14 +3,14 @@ import abc import typing as t -from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike from ._common import DatatypeCallback, ensure_datatype -class StorageGatewayDocument(utils.PayloadWrapper): +class StorageGatewayDocument(Payload): """ Convenience class for constructing a Storage Gateway document to use as the `data` parameter to ``create_storage_gateway`` or @@ -91,7 +91,7 @@ def __init__( ensure_datatype(self) -class StorageGatewayPolicies(utils.PayloadWrapper, abc.ABC): +class StorageGatewayPolicies(Payload, abc.ABC): """ This is the abstract base type for Storage Policies documents to use as the ``policies`` parameter when creating a StorageGatewayDocument. diff --git a/src/globus_sdk/services/gcs/data/user_credential.py b/src/globus_sdk/services/gcs/data/user_credential.py index da9be19f1..ecfef3d5e 100644 --- a/src/globus_sdk/services/gcs/data/user_credential.py +++ b/src/globus_sdk/services/gcs/data/user_credential.py @@ -2,12 +2,12 @@ import typing as t -from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike -class UserCredentialDocument(utils.PayloadWrapper): +class UserCredentialDocument(Payload): """ Convenience class for constructing a UserCredential document to use as the `data` parameter to `create_user_credential` and diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index 77bdc1d8b..41b0649bc 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -5,6 +5,7 @@ from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike T = t.TypeVar("T") @@ -98,7 +99,7 @@ def _docstring_fixer(cls: type[T]) -> type[T]: return cls -class BatchMembershipActions(utils.PayloadWrapper): +class BatchMembershipActions(Payload): """ An object used to represent a batch action on memberships of a group. `Perform actions on group members @@ -255,7 +256,7 @@ def request_join( @_docstring_fixer -class GroupPolicies(utils.PayloadWrapper): +class GroupPolicies(Payload): """ An object used to represent the policy settings of a group. This may be used to set or modify group settings. diff --git a/src/globus_sdk/services/search/data.py b/src/globus_sdk/services/search/data.py index 346337ca3..72baa0b88 100644 --- a/src/globus_sdk/services/search/data.py +++ b/src/globus_sdk/services/search/data.py @@ -2,8 +2,9 @@ import typing as t -from globus_sdk import exc, utils +from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload # workaround for absence of Self type # for the workaround and some background, see: @@ -21,7 +22,7 @@ def _format_histogram_range( # an internal class for declaring multiple related types with shared methods -class SearchQueryBase(utils.PayloadWrapper): +class SearchQueryBase(Payload): """ The base class for all Search query helpers. @@ -226,7 +227,7 @@ def add_sort( return self -class SearchQueryV1(utils.PayloadWrapper): +class SearchQueryV1(Payload): """ A specialized dict which has helpers for creating and modifying a Search Query document. Replaces the usage of ``SearchQuery``. diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index 2ba105009..30a893ae6 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -7,15 +7,16 @@ import typing as t from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk.config import get_service_url from globus_sdk.exc import warn_deprecated from globus_sdk.services.transfer import TransferData -from globus_sdk.utils import PayloadWrapper, slash_join +from globus_sdk.utils import slash_join log = logging.getLogger(__name__) -class TransferTimer(PayloadWrapper): +class TransferTimer(Payload): """ A helper for defining a payload for Transfer Timer creation. Use this along with :meth:`create_timer ` to @@ -124,7 +125,7 @@ def _preprocess_body( return new_body -class RecurringTimerSchedule(PayloadWrapper): +class RecurringTimerSchedule(Payload): """ A helper used as part of a *timer* to define when the *timer* will run. @@ -182,7 +183,7 @@ def __init__( } -class OnceTimerSchedule(PayloadWrapper): +class OnceTimerSchedule(Payload): """ A helper used as part of a *timer* to define when the *timer* will run. @@ -202,7 +203,7 @@ def __init__( self["datetime"] = _format_date(datetime) -class TimerJob(PayloadWrapper): +class TimerJob(Payload): r""" .. warning:: diff --git a/src/globus_sdk/services/transfer/data/__init__.py b/src/globus_sdk/services/transfer/data/__init__.py index 92b79a37c..b5904588a 100644 --- a/src/globus_sdk/services/transfer/data/__init__.py +++ b/src/globus_sdk/services/transfer/data/__init__.py @@ -1,8 +1,7 @@ """ Data helper classes for constructing Transfer API documents. All classes should -be PayloadWrapper types, so they can be passed seamlessly to -:class:`TransferClient ` methods without -conversion. +be Payload types, so they can be passed seamlessly to +:class:`TransferClient ` methods without conversion. """ from .delete_data import DeleteData diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index ec4972dd5..da93eb4fc 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -4,8 +4,9 @@ import logging import typing as t -from globus_sdk import exc, utils +from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike if t.TYPE_CHECKING: @@ -14,7 +15,7 @@ log = logging.getLogger(__name__) -class DeleteData(utils.PayloadWrapper): +class DeleteData(Payload): r""" Convenience class for constructing a delete document, to use as the `data` parameter to diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index cb31f6ca0..7ab25bd0a 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -4,8 +4,9 @@ import logging import typing as t -from globus_sdk import exc, utils +from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import Payload from globus_sdk._types import UUIDLike if t.TYPE_CHECKING: @@ -36,7 +37,7 @@ def _parse_sync_level( return sync_level -class TransferData(utils.PayloadWrapper): +class TransferData(Payload): r""" Convenience class for constructing a transfer document, to use as the ``data`` parameter to diff --git a/src/globus_sdk/transport/encoders.py b/src/globus_sdk/transport/encoders.py index 359564136..5052f47e9 100644 --- a/src/globus_sdk/transport/encoders.py +++ b/src/globus_sdk/transport/encoders.py @@ -6,7 +6,6 @@ import requests -from globus_sdk import utils from globus_sdk._missing import MISSING, filter_missing @@ -87,12 +86,12 @@ def _prepare_data(self, data: t.Any) -> t.Any: """ Prepare the data (body) for a request. - If the body is a dict or PayloadWrapper, it will be recursively processed to + If the body is a dict, list, or tuple, it will be recursively processed to filter out MISSING and format primitives. Otherwise, it is returned as-is. """ - if isinstance(data, (dict, utils.PayloadWrapper)): + if isinstance(data, dict): return filter_missing({k: self._prepare_data(v) for k, v in data.items()}) elif isinstance(data, (list, tuple)): return [self._prepare_data(x) for x in data if x is not MISSING] @@ -139,7 +138,7 @@ def encode( data: t.Any, headers: dict[str, str], ) -> requests.Request: - if not isinstance(data, (dict, utils.PayloadWrapper)): + if not isinstance(data, dict): raise TypeError("FormRequestEncoder cannot encode non-dict data") return requests.Request( method, diff --git a/src/globus_sdk/transport/requests.py b/src/globus_sdk/transport/requests.py index cfac09b55..0531e42f5 100644 --- a/src/globus_sdk/transport/requests.py +++ b/src/globus_sdk/transport/requests.py @@ -9,7 +9,7 @@ import requests -from globus_sdk import __version__, config, exc, utils +from globus_sdk import __version__, config, exc from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.transport.encoders import ( FormRequestEncoder, @@ -253,9 +253,7 @@ def _encode( method: str, url: str, query_params: dict[str, t.Any] | None = None, - data: ( - dict[str, t.Any] | list[t.Any] | utils.PayloadWrapper | str | bytes | None - ) = None, + data: dict[str, t.Any] | list[t.Any] | str | bytes | None = None, headers: dict[str, str] | None = None, encoding: str | None = None, ) -> requests.Request: @@ -305,9 +303,7 @@ def request( method: str, url: str, query_params: dict[str, t.Any] | None = None, - data: ( - dict[str, t.Any] | list[t.Any] | utils.PayloadWrapper | str | bytes | None - ) = None, + data: dict[str, t.Any] | list[t.Any] | str | bytes | None = None, headers: dict[str, str] | None = None, encoding: str | None = None, authorizer: GlobusAuthorizer | None = None, diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index 22c553c2f..bb998a7c2 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -7,15 +7,9 @@ import typing as t import uuid -from globus_sdk._missing import MISSING, MissingType +from globus_sdk._missing import MissingType from globus_sdk._types import UUIDLike -if t.TYPE_CHECKING: - # pylint: disable=unsubscriptable-object - PayloadWrapperBase = collections.UserDict[str, t.Any] -else: - PayloadWrapperBase = collections.UserDict - def sha256_string(s: str) -> str: return hashlib.sha256(s.encode("utf-8")).hexdigest() @@ -89,87 +83,3 @@ def commajoin(val: UUIDLike | t.Iterable[UUIDLike] | MissingType) -> str | Missi if isinstance(val, collections.abc.Iterable): return ",".join(safe_strseq_iter(val)) return str(val) - - -class PayloadWrapper(PayloadWrapperBase): - """ - A class for defining helper objects which wrap some kind of "payload" dict. - Typical for helper objects which formulate a request payload, e.g. as JSON. - - Payload types inheriting from this class can be passed directly to the client - ``post()``, ``put()``, and ``patch()`` methods instead of a dict. These methods will - recognize a ``PayloadWrapper`` and convert it to a dict for serialization with the - requested encoder (e.g. as a JSON request body). - """ - - # use UserDict rather than subclassing dict so that our API is always consistent - # e.g. `dict.pop` does not invoke `dict.__delitem__`. Overriding `__delitem__` on a - # dict subclass can lead to inconsistent behavior between usages like these: - # x = d["k"]; del d["k"] - # x = d.pop("k") - # - # UserDict inherits from MutableMapping and only defines the dunder methods, so - # changing its behavior safely/consistently is simpler - - # - # internal helpers for setting non-null values - # - - def _set_value( - self, - key: str, - val: t.Any, - callback: t.Callable[[t.Any], t.Any] | None = None, - ) -> None: - """ - Internal helper for setting an omittable value on the payload. - - If the value is non-None, it will be set and the callback (if provided) will be - invoked on it. - Otherwise, it will be ignored and the callback will not be invoked. - - :param key: The key to set. - :param val: The value to set. - :param callback: An optional callback to apply to the value immediately - before it is set. - """ - if val is not None and val is not MISSING: - self[key] = callback(val) if callback else val - - def _set_optstrs(self, **kwargs: t.Any) -> None: - """ - Convenience function for setting a collection of omittable string values. - - Values are converted to strings prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=str) - - def _set_optstrlists( - self, **kwargs: t.Iterable[t.Any] | None | MissingType - ) -> None: - """ - Convenience function for setting a collection of omittable string list values. - - Values are converted to lists of strings prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=lambda x: list(safe_strseq_iter(x))) - - def _set_optbools(self, **kwargs: bool | None | MissingType) -> None: - """ - Convenience function for setting a collection of omittable bool values. - - Values are converted to bools prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=bool) - - def _set_optints(self, **kwargs: t.Any) -> None: - """ - Convenience function for setting a collection of omittable int values. - - Values are converted to ints prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=int) diff --git a/tests/unit/helpers/gcs/test_collections.py b/tests/unit/helpers/gcs/test_collections.py index 70f1b2b05..f90ef8bb7 100644 --- a/tests/unit/helpers/gcs/test_collections.py +++ b/tests/unit/helpers/gcs/test_collections.py @@ -21,8 +21,8 @@ STUB_UC_ID = uuid.uuid1() # user credential -MappedCollectionSignature = inspect.signature(MappedCollectionDocument) -GuestCollectionSignature = inspect.signature(GuestCollectionDocument) +MappedCollectionSignature = inspect.signature(MappedCollectionDocument.__init__) +GuestCollectionSignature = inspect.signature(GuestCollectionDocument.__init__) def test_collection_base_abstract(): diff --git a/tests/unit/helpers/test_timer.py b/tests/unit/helpers/test_timer.py index b6bff6c1b..ba8a51202 100644 --- a/tests/unit/helpers/test_timer.py +++ b/tests/unit/helpers/test_timer.py @@ -76,7 +76,7 @@ def test_transfer_timer_removes_disallowed_fields(): ) def test_once_timer_schedule_formats_datetime(input_time, expected): schedule = OnceTimerSchedule(datetime=input_time) - assert schedule.data == {"type": "once", "datetime": expected} + assert dict(schedule) == {"type": "once", "datetime": expected} def test_recurring_timer_schedule_interval_only(): diff --git a/tests/unit/test_payload.py b/tests/unit/test_payload.py new file mode 100644 index 000000000..ded48d6ee --- /dev/null +++ b/tests/unit/test_payload.py @@ -0,0 +1,73 @@ +import abc + +import pytest + +from globus_sdk._payload import AbstractPayload, Payload + + +def test_payload_methods(): + # just make sure that PayloadWrapper acts like a dict... + data = Payload() + assert "foo" not in data + with pytest.raises(KeyError): + data["foo"] + data["foo"] = 1 + assert "foo" in data + assert data["foo"] == 1 + del data["foo"] + assert "foo" not in data + assert len(data) == 0 + assert list(data) == [] + data["foo"] = 1 + data["bar"] = 2 + assert len(data) == 2 + assert data == {"foo": 1, "bar": 2} + data.update({"x": "hello", "y": "world"}) + assert data == {"foo": 1, "bar": 2, "x": "hello", "y": "world"} + + +def test_abstract_payload_detects_abstract_methods(): + # A has no abstract methods so it will instantiate + class A(AbstractPayload): + pass + + A() + + # B has an abstract method and inherits from AbstractPayload so it should + # fail to instantiate + class B(A): + @abc.abstractmethod + def f(self): ... + + with pytest.raises( + TypeError, + match=( + "Can't instantiate abstract class B without an " + "implementation for abstract method 'f'" + ), + ): + B() + + # C has two abstract methods, so these should be listed comma separated + class C(B): + @abc.abstractmethod + def g(self): ... + + with pytest.raises( + TypeError, + match=( + "Can't instantiate abstract class C without an " + "implementation for abstract methods ('f', 'g'|'g', 'f')" + ), + ): + C() + + # D should be instantiable because it defines the abstract methods + class D(C): + def f(self): + return 1 + + def g(self): + return 2 + + D() diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 61cca022e..4b0edd847 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -45,27 +45,6 @@ def test_slash_join(a, b): assert utils.slash_join(a, b) == "a/b" -def test_payload_wrapper_methods(): - # just make sure that PayloadWrapper acts like a dict... - data = utils.PayloadWrapper() - assert "foo" not in data - with pytest.raises(KeyError): - data["foo"] - data["foo"] = 1 - assert "foo" in data - assert data["foo"] == 1 - del data["foo"] - assert "foo" not in data - assert len(data) == 0 - assert list(data) == [] - data["foo"] = 1 - data["bar"] = 2 - assert len(data) == 2 - assert data.data == {"foo": 1, "bar": 2} - data.update({"x": "hello", "y": "world"}) - assert data.data == {"foo": 1, "bar": 2, "x": "hello", "y": "world"} - - @pytest.mark.parametrize( "value, expected_result", ( diff --git a/tests/unit/transport/test_transport_encoders.py b/tests/unit/transport/test_transport_encoders.py index a06613f01..351e39398 100644 --- a/tests/unit/transport/test_transport_encoders.py +++ b/tests/unit/transport/test_transport_encoders.py @@ -3,8 +3,8 @@ import pytest from globus_sdk import MISSING +from globus_sdk._payload import Payload from globus_sdk.transport import FormRequestEncoder, JSONRequestEncoder, RequestEncoder -from globus_sdk.utils import PayloadWrapper @pytest.mark.parametrize("data", ("foo", b"bar")) @@ -71,7 +71,7 @@ def test_all_request_encoders_remove_missing_in_params_and_headers(encoder_class @pytest.mark.parametrize( - "using_payload_wrapper, payload_contents, expected_data", + "using_payload_type, payload_contents, expected_data", [ # basic dicts (False, {"foo": 1}, {"foo": 1}), @@ -85,7 +85,7 @@ def test_all_request_encoders_remove_missing_in_params_and_headers(encoder_class # nested payload wrappers (get dictified / "unwrapped") ( True, - {"bar": PayloadWrapper(foo=1), "baz": [2, PayloadWrapper(foo=1)]}, + {"bar": Payload(foo=1), "baz": [2, Payload(foo=1)]}, {"bar": {"foo": 1}, "baz": [2, {"foo": 1}]}, ), # document with UUIDs and tuples buried inside nested structures @@ -97,10 +97,10 @@ def test_all_request_encoders_remove_missing_in_params_and_headers(encoder_class ], ) def test_json_encoder_payload_preparation( - using_payload_wrapper, payload_contents, expected_data + using_payload_type, payload_contents, expected_data ): encoder = JSONRequestEncoder() - x = PayloadWrapper() if using_payload_wrapper else {} + x = Payload() if using_payload_type else {} for k, v in payload_contents.items(): x[k] = v request = encoder.encode( @@ -131,7 +131,7 @@ def test_json_encoder_is_well_defined_on_array_containing_missing(): @pytest.mark.parametrize( - "using_payload_wrapper, payload_contents, expected_data", + "using_payload_type, payload_contents, expected_data", [ # basic dicts (True, {"foo": 1}, {"foo": 1}), @@ -145,10 +145,10 @@ def test_json_encoder_is_well_defined_on_array_containing_missing(): ], ) def test_form_encoder_payload_preparation( - using_payload_wrapper, payload_contents, expected_data + using_payload_type, payload_contents, expected_data ): encoder = FormRequestEncoder() - x = PayloadWrapper() if using_payload_wrapper else {} + x = Payload() if using_payload_type else {} for k, v in payload_contents.items(): x[k] = v request = encoder.encode( From cd64e4232c9c6f217a883291e08fdea69ce46fe7 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 10 Jun 2025 15:56:26 -0500 Subject: [PATCH 037/176] Move tools of 'utils' and into '_remarshal' To further break down 'utils' bloat and establish a clear home for a flavor of utility function, introduce `globus_sdk._remarshal` as a place for helpers which reshape data from one form into another. `safe_strseq_iter` and `commajoin` move into this immediately. (`slash_join` does not), and `safe_strseq_listify` is added as a very common case. Usages and unit tests are updated. --- src/globus_sdk/_payload.py | 2 +- src/globus_sdk/_remarshal.py | 86 +++++++++++++++++++ .../services/auth/client/base_login_client.py | 9 +- .../auth/client/confidential_client.py | 11 +-- .../services/auth/client/service_client.py | 23 ++--- src/globus_sdk/services/compute/client.py | 8 +- src/globus_sdk/services/flows/client.py | 8 +- src/globus_sdk/services/gcs/client.py | 11 +-- .../services/gcs/data/collection.py | 44 ++-------- src/globus_sdk/services/gcs/data/endpoint.py | 8 +- src/globus_sdk/services/groups/client.py | 5 +- src/globus_sdk/services/groups/data.py | 22 ++--- src/globus_sdk/services/search/client.py | 5 +- src/globus_sdk/services/transfer/client.py | 13 +-- src/globus_sdk/utils.py | 43 ---------- tests/unit/helpers/gcs/test_collections.py | 2 +- tests/unit/test_remarshal.py | 57 ++++++++++++ tests/unit/test_utils.py | 15 ---- 18 files changed, 217 insertions(+), 155 deletions(-) create mode 100644 src/globus_sdk/_remarshal.py create mode 100644 tests/unit/test_remarshal.py diff --git a/src/globus_sdk/_payload.py b/src/globus_sdk/_payload.py index 917bed1a7..0e8fd2e1a 100644 --- a/src/globus_sdk/_payload.py +++ b/src/globus_sdk/_payload.py @@ -4,7 +4,7 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk.utils import safe_strseq_iter +from globus_sdk._remarshal import safe_strseq_iter if t.TYPE_CHECKING: # pylint: disable=unsubscriptable-object diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py new file mode 100644 index 000000000..a0bd104b5 --- /dev/null +++ b/src/globus_sdk/_remarshal.py @@ -0,0 +1,86 @@ +""" +This module provides internal helpers for remarshalling data from one shape +into another. + +This may be as simple as converting strings from one form to another, or as +sophisticated as building a specific internal object in a configurable way. +""" + +from __future__ import annotations + +import collections.abc +import typing as t +import uuid + +from globus_sdk._missing import MISSING, MissingType +from globus_sdk._types import UUIDLike + + +def safe_strseq_iter( + value: t.Iterable[t.Any] | str | uuid.UUID, +) -> t.Iterator[str]: + """ + Given an Iterable (typically of strings), produce an iterator over it of strings. + + :param value: The stringifiable object or objects to iterate over + + This is a passthrough with some caveats: + - if the value is a solitary string, yield only that value + - if the value is a solitary UUID, yield only that value (as a string) + - str values in the iterable which are not strings + + This helps handle cases where a string is passed to a function expecting an iterable + of strings, as well as cases where an iterable of UUID objects is accepted for a + list of IDs, or something similar. + """ + if isinstance(value, str): + yield value + elif isinstance(value, uuid.UUID): + yield str(value) + else: + for x in value: + yield str(x) + + +@t.overload +def safe_strseq_listify(value: None) -> None: ... +@t.overload +def safe_strseq_listify(value: MissingType) -> MissingType: ... +@t.overload +def safe_strseq_listify(value: t.Iterable[t.Any] | str | uuid.UUID) -> list[str]: ... + + +def safe_strseq_listify( + value: t.Iterable[t.Any] | str | uuid.UUID | MissingType | None, +) -> list[str] | MissingType | None: + """ + A wrapper over safe_strseq_iter which produces list outputs. + This method takes responsibility for checking for MISSING and None values. + + Unlike safe_strseq_iter, this may be the "last mile" remarshalling step before + data is actually passed to the network layer. Therefore, it makes sense for this + helper to handle (MISSING | None). + """ + if value is None: + return None + if isinstance(value, MissingType): + return MISSING + return list(safe_strseq_iter(value)) + + +@t.overload +def commajoin(value: MissingType) -> MissingType: ... +@t.overload +def commajoin(value: UUIDLike | t.Iterable[UUIDLike]) -> str: ... + + +def commajoin( + value: UUIDLike | t.Iterable[UUIDLike] | MissingType, +) -> str | MissingType: + # note that this explicit handling of Iterable allows for string-like objects to be + # passed to this function and be stringified by the `str()` call + if isinstance(value, MissingType): + return value + if isinstance(value, collections.abc.Iterable): + return ",".join(safe_strseq_iter(value)) + return str(value) diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 3756a4156..ab9bb9116 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -5,7 +5,8 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey -from globus_sdk import _guards, client, exc, utils +from globus_sdk import _guards, client, exc +from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer, NullAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -182,15 +183,15 @@ def oauth2_get_authorize_url( if query_params is None: query_params = {} if session_required_identities is not None: - query_params["session_required_identities"] = utils.commajoin( + query_params["session_required_identities"] = commajoin( session_required_identities ) if session_required_single_domain is not None: - query_params["session_required_single_domain"] = utils.commajoin( + query_params["session_required_single_domain"] = commajoin( session_required_single_domain ) if session_required_policies is not None: - query_params["session_required_policies"] = utils.commajoin( + query_params["session_required_policies"] = commajoin( session_required_policies ) if session_required_mfa is not None: diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 5e6672d9c..a2c08b7b5 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -3,8 +3,9 @@ import logging import typing as t -from globus_sdk import exc, utils +from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import commajoin, safe_strseq_iter, safe_strseq_listify from globus_sdk._types import ScopeCollectionType, UUIDLike from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -96,12 +97,12 @@ def get_identities( query_params = {} if usernames is not None: - query_params["usernames"] = utils.commajoin(usernames) + query_params["usernames"] = commajoin(usernames) query_params["provision"] = ( "false" if str(provision).lower() == "false" else "true" ) if ids is not None: - query_params["ids"] = utils.commajoin(ids) + query_params["ids"] = commajoin(ids) return GetIdentitiesResponse( self.get("/v2/api/identities", query_params=query_params) @@ -270,7 +271,7 @@ def oauth2_get_dependent_tokens( if refresh_tokens: form_data["access_type"] = "offline" if not isinstance(scope, MissingType): - form_data["scope"] = " ".join(utils.safe_strseq_iter(scope)) + form_data["scope"] = " ".join(safe_strseq_iter(scope)) if additional_params: form_data.update(additional_params) @@ -456,7 +457,7 @@ def create_child_client( "client_type": client_type, } if not isinstance(redirect_uris, MissingType): - body["redirect_uris"] = list(utils.safe_strseq_iter(redirect_uris)) + body["redirect_uris"] = safe_strseq_listify(redirect_uris) # terms_and_conditions and privacy_policy must both be set or unset if bool(terms_and_conditions) ^ bool(privacy_policy): diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index daa17d4cc..4e9940f47 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -6,8 +6,9 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey -from globus_sdk import client, exc, utils +from globus_sdk import client, exc from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import commajoin, safe_strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.response import GlobusHTTPResponse, IterableResponse @@ -352,12 +353,12 @@ def get_identities( # if either of these params has a truthy value, stringify it if usernames: - query_params["usernames"] = utils.commajoin(usernames) + query_params["usernames"] = commajoin(usernames) query_params["provision"] = ( "false" if str(provision).lower() == "false" else "true" ) if ids: - query_params["ids"] = utils.commajoin(ids) + query_params["ids"] = commajoin(ids) log.debug(f"query_params={query_params}") @@ -453,9 +454,9 @@ def get_identity_providers( # letting us consume args whose `__str__` methods produce "the right # thing" elif domains is not None: - query_params["domains"] = utils.commajoin(domains) + query_params["domains"] = commajoin(domains) elif ids is not None: - query_params["ids"] = utils.commajoin(ids) + query_params["ids"] = commajoin(ids) else: log.warning( "neither 'domains' nor 'ids' provided to get_identity_providers(). " @@ -623,9 +624,9 @@ def create_project( "contact_email": contact_email, } if admin_ids is not None: - body["admin_ids"] = list(utils.safe_strseq_iter(admin_ids)) + body["admin_ids"] = safe_strseq_listify(admin_ids) if admin_group_ids is not None: - body["admin_group_ids"] = list(utils.safe_strseq_iter(admin_group_ids)) + body["admin_group_ids"] = safe_strseq_listify(admin_group_ids) return self.post("/v2/api/projects", data={"project": body}) def update_project( @@ -679,9 +680,9 @@ def update_project( if contact_email is not None: body["contact_email"] = contact_email if admin_ids is not None: - body["admin_ids"] = list(utils.safe_strseq_iter(admin_ids)) + body["admin_ids"] = safe_strseq_listify(admin_ids) if admin_group_ids is not None: - body["admin_group_ids"] = list(utils.safe_strseq_iter(admin_group_ids)) + body["admin_group_ids"] = safe_strseq_listify(admin_group_ids) return self.put(f"/v2/api/projects/{project_id}", data={"project": body}) def delete_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: @@ -1647,9 +1648,9 @@ def get_scopes( query_params = {} if not isinstance(scope_strings, MissingType): - query_params["scope_strings"] = utils.commajoin(scope_strings) + query_params["scope_strings"] = commajoin(scope_strings) if not isinstance(ids, MissingType): - query_params["ids"] = utils.commajoin(ids) + query_params["ids"] = commajoin(ids) return GetScopesResponse(self.get("/v2/api/scopes", query_params=query_params)) diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index ccc32e4bf..abd7c2f49 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -3,8 +3,9 @@ import logging import typing as t -from globus_sdk import GlobusHTTPResponse, client, utils +from globus_sdk import GlobusHTTPResponse, client from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import safe_strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.scopes import ComputeScopes, Scope @@ -223,8 +224,9 @@ def get_task_batch( :service: compute :ref: Root/operation/get_batch_status_v2_batch_status_post """ - task_ids = list(utils.safe_strseq_iter(task_ids)) - return self.post("/v2/batch_status", data={"task_ids": task_ids}) + return self.post( + "/v2/batch_status", data={"task_ids": safe_strseq_listify(task_ids)} + ) def get_task_group(self, task_group_id: UUIDLike) -> GlobusHTTPResponse: """Get a list of task IDs associated with a task group. diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index a48f9e145..a4e764284 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -12,9 +12,9 @@ client, exc, paging, - utils, ) from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp @@ -361,7 +361,7 @@ def list_flows( raise GlobusSDKUsageError(msg) query_params = { "filter_role": filter_role, - "filter_roles": utils.commajoin(filter_roles), + "filter_roles": commajoin(filter_roles), "filter_fulltext": filter_fulltext, # if `orderby` is an iterable (e.g., generator expression), it gets # converted to a list in this step @@ -654,8 +654,8 @@ def list_runs( :ref: Runs/paths/~1runs/get """ query_params = { - "filter_flow_id": utils.commajoin(filter_flow_id), - "filter_roles": utils.commajoin(filter_roles), + "filter_flow_id": commajoin(filter_flow_id), + "filter_roles": commajoin(filter_roles), "marker": marker, **(query_params or {}), } diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 0636d1229..31d89cebf 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -6,6 +6,7 @@ from globus_sdk import client, exc, paging, response, scopes, utils from globus_sdk._classproperty import classproperty from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp @@ -258,7 +259,7 @@ def update_endpoint( :ref: openapi_Endpoint/#patchEndpoint :service: gcs """ - query_params = {"include": utils.commajoin(include), **(query_params or {})} + query_params = {"include": commajoin(include), **(query_params or {})} return UnpackingGCSResponse( self.patch( "/endpoint", @@ -313,11 +314,11 @@ def get_collection_list( :service: gcs """ query_params = { - "include": utils.commajoin(include), + "include": commajoin(include), "page_size": page_size, "marker": marker, "mapped_collection_id": mapped_collection_id, - "filter": utils.commajoin(filter), + "filter": commajoin(filter), **(query_params or {}), } return IterableGCSResponse(self.get("collections", query_params=query_params)) @@ -480,7 +481,7 @@ def get_storage_gateway_list( :service: gcs """ query_params = { - "include": utils.commajoin(include), + "include": commajoin(include), "page_size": page_size, "marker": marker, **(query_params or {}), @@ -544,7 +545,7 @@ def get_storage_gateway( :ref: openapi_Storage_Gateways/#getStorageGateway :service: gcs """ - query_params = {"include": utils.commajoin(include), **(query_params or {})} + query_params = {"include": commajoin(include), **(query_params or {})} return UnpackingGCSResponse( self.get( f"/storage_gateways/{storage_gateway_id}", diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index 2ac4866e4..627bb3178 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -3,9 +3,9 @@ import abc import typing as t -from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractPayload +from globus_sdk._remarshal import safe_strseq_listify from globus_sdk._types import UUIDLike from ._common import ( @@ -198,11 +198,7 @@ def __init__( ) self["user_message"] = user_message self["user_message_link"] = user_message_link - self["keywords"] = ( - keywords - if isinstance(keywords, MissingType) - else list(utils.safe_strseq_iter(keywords)) - ) + self["keywords"] = safe_strseq_listify(keywords) self["disable_verify"] = disable_verify self["enable_https"] = enable_https self["force_encryption"] = force_encryption @@ -361,16 +357,8 @@ def __init__( self["guest_auth_policy_id"] = guest_auth_policy_id self["storage_gateway_id"] = storage_gateway_id - self["sharing_users_allow"] = ( - sharing_users_allow - if isinstance(sharing_users_allow, (MissingType, type(None))) - else list(utils.safe_strseq_iter(sharing_users_allow)) - ) - self["sharing_users_deny"] = ( - sharing_users_deny - if isinstance(sharing_users_deny, (MissingType, type(None))) - else list(utils.safe_strseq_iter(sharing_users_deny)) - ) + self["sharing_users_allow"] = safe_strseq_listify(sharing_users_allow) + self["sharing_users_deny"] = safe_strseq_listify(sharing_users_deny) self["delete_protected"] = delete_protected self["allow_guest_collections"] = allow_guest_collections @@ -531,16 +519,8 @@ def __init__( super().__init__() self["DATA_TYPE"] = DATA_TYPE - self["sharing_groups_allow"] = ( - sharing_groups_allow - if isinstance(sharing_groups_allow, (MissingType, type(None))) - else list(utils.safe_strseq_iter(sharing_groups_allow)) - ) - self["sharing_groups_deny"] = ( - sharing_groups_deny - if isinstance(sharing_groups_deny, (MissingType, type(None))) - else list(utils.safe_strseq_iter(sharing_groups_deny)) - ) + self["sharing_groups_allow"] = safe_strseq_listify(sharing_groups_allow) + self["sharing_groups_deny"] = safe_strseq_listify(sharing_groups_deny) if not isinstance(additional_fields, MissingType): self.update(additional_fields) @@ -570,16 +550,8 @@ def __init__( ) -> None: super().__init__() self["DATA_TYPE"] = DATA_TYPE - self["sharing_groups_allow"] = ( - sharing_groups_allow - if isinstance(sharing_groups_allow, (MissingType, type(None))) - else list(utils.safe_strseq_iter(sharing_groups_allow)) - ) - self["sharing_groups_deny"] = ( - sharing_groups_deny - if isinstance(sharing_groups_deny, (MissingType, type(None))) - else list(utils.safe_strseq_iter(sharing_groups_deny)) - ) + self["sharing_groups_allow"] = safe_strseq_listify(sharing_groups_allow) + self["sharing_groups_deny"] = safe_strseq_listify(sharing_groups_deny) if not isinstance(additional_fields, MissingType): self.update(additional_fields) diff --git a/src/globus_sdk/services/gcs/data/endpoint.py b/src/globus_sdk/services/gcs/data/endpoint.py index da2f55fe5..5a7fc234a 100644 --- a/src/globus_sdk/services/gcs/data/endpoint.py +++ b/src/globus_sdk/services/gcs/data/endpoint.py @@ -2,9 +2,9 @@ import typing as t -from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import Payload +from globus_sdk._remarshal import safe_strseq_listify from globus_sdk.services.gcs.data._common import DatatypeCallback, ensure_datatype @@ -127,11 +127,7 @@ def __init__( self["info_link"] = info_link self["network_use"] = network_use self["organization"] = organization - self["keywords"] = ( - keywords - if isinstance(keywords, MissingType) - else list(utils.safe_strseq_iter(keywords)) - ) + self["keywords"] = safe_strseq_listify(keywords) self["allow_udt"] = allow_udt self["public"] = public self["max_concurrency"] = max_concurrency diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index 623a12cd6..48531007e 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -2,8 +2,9 @@ import typing as t -from globus_sdk import client, response, utils +from globus_sdk import client, response from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.scopes import GroupsScopes, Scope @@ -81,7 +82,7 @@ def get_group( :service: groups :ref: get_group_v2_groups__group_id__get """ - query_params = {"include": utils.commajoin(include), **(query_params or {})} + query_params = {"include": commajoin(include), **(query_params or {})} return self.get(f"/v2/groups/{group_id}", query_params=query_params) def get_group_by_subscription_id( diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index 41b0649bc..f884c41a7 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -3,9 +3,9 @@ import enum import typing as t -from globus_sdk import utils from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import Payload +from globus_sdk._remarshal import safe_strseq_iter from globus_sdk._types import UUIDLike T = t.TypeVar("T") @@ -117,7 +117,7 @@ def accept_invites( """ self.setdefault("accept", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -135,7 +135,7 @@ def add_members( """ self.setdefault("add", []).extend( {"identity_id": identity_id, "role": role} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -149,7 +149,7 @@ def approve_pending( """ self.setdefault("approve", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -163,7 +163,7 @@ def decline_invites( """ self.setdefault("decline", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -181,7 +181,7 @@ def invite_members( """ self.setdefault("invite", []).extend( {"identity_id": identity_id, "role": role} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -194,7 +194,7 @@ def join(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: """ self.setdefault("join", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -207,7 +207,7 @@ def leave(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: """ self.setdefault("leave", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -221,7 +221,7 @@ def reject_join_requests( """ self.setdefault("reject", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -236,7 +236,7 @@ def remove_members( """ self.setdefault("remove", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self @@ -250,7 +250,7 @@ def request_join( """ self.setdefault("request_join", []).extend( {"identity_id": identity_id} - for identity_id in utils.safe_strseq_iter(identity_ids) + for identity_id in safe_strseq_iter(identity_ids) ) return self diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index f2068e1ba..466c321e3 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -3,8 +3,9 @@ import logging import typing as t -from globus_sdk import client, paging, response, utils +from globus_sdk import client, paging, response from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import safe_strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import Scope, SearchScopes @@ -575,7 +576,7 @@ def batch_delete_by_subject( # ensure that a single string is *not* treated as an iterable of strings, # which is usually not intentional body = { - "subjects": list(utils.safe_strseq_iter(subjects)), + "subjects": safe_strseq_listify(subjects), **(additional_params or {}), } return self.post(f"/v1/index/{index_id}/batch_delete_by_subject", data=body) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 446a33fae..00e21d31f 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -5,8 +5,9 @@ import typing as t import uuid -from globus_sdk import _guards, client, exc, paging, response, utils +from globus_sdk import _guards, client, exc, paging, response from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import commajoin from globus_sdk._types import DateLike, IntLike, UUIDLike from globus_sdk.scopes import GCSCollectionScopeBuilder, Scope, TransferScopes @@ -50,7 +51,7 @@ def _format_filter_item(x: str | TransferFilterDict | MissingType) -> str | Miss return MISSING elif isinstance(x, str): return x - return "/".join(f"{k}:{utils.commajoin(v)}" for k, v in x.items()) + return "/".join(f"{k}:{commajoin(v)}" for k, v in x.items()) def _format_filter( @@ -1308,7 +1309,7 @@ def operation_ls( if show_hidden else 0 if isinstance(show_hidden, bool) else show_hidden ), - "orderby": utils.commajoin(orderby), + "orderby": commajoin(orderby), "filter": _format_filter(filter), "local_user": local_user, **(query_params or {}), @@ -1726,7 +1727,7 @@ def task_list( query_params = { "limit": limit, "offset": offset, - "orderby": utils.commajoin(orderby), + "orderby": commajoin(orderby), "filter": _format_filter_item(filter), **(query_params or {}), } @@ -2346,8 +2347,8 @@ def endpoint_manager_task_list( "also supplied." ) query_params = { - "filter_status": utils.commajoin(filter_status), - "filter_task_id": utils.commajoin(filter_task_id), + "filter_status": commajoin(filter_status), + "filter_task_id": commajoin(filter_task_id), "filter_owner_id": filter_owner_id, "filter_endpoint": filter_endpoint, "filter_endpoint_use": filter_endpoint_use, diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/utils.py index bb998a7c2..5930d676a 100644 --- a/src/globus_sdk/utils.py +++ b/src/globus_sdk/utils.py @@ -1,14 +1,7 @@ from __future__ import annotations -import collections -import collections.abc import hashlib import platform -import typing as t -import uuid - -from globus_sdk._missing import MissingType -from globus_sdk._types import UUIDLike def sha256_string(s: str) -> str: @@ -47,39 +40,3 @@ def slash_join(a: str, b: str | None) -> str: if b.startswith("/"): return a + b return a + "/" + b - - -def safe_strseq_iter( - value: t.Iterable[t.Any] | str | uuid.UUID, -) -> t.Iterator[str]: - """ - Given an Iterable (typically of strings), produce an iterator over it of strings. - - :param value: The stringifiable object or objects to iterate over - - This is a passthrough with some caveats: - - if the value is a solitary string, yield only that value - - if the value is a solitary UUID, yield only that value (as a string) - - str values in the iterable which are not strings - - This helps handle cases where a string is passed to a function expecting an iterable - of strings, as well as cases where an iterable of UUID objects is accepted for a - list of IDs, or something similar. - """ - if isinstance(value, str): - yield value - elif isinstance(value, uuid.UUID): - yield str(value) - else: - for x in value: - yield str(x) - - -def commajoin(val: UUIDLike | t.Iterable[UUIDLike] | MissingType) -> str | MissingType: - # note that this explicit handling of Iterable allows for string-like objects to be - # passed to this function and be stringified by the `str()` call - if isinstance(val, MissingType): - return val - if isinstance(val, collections.abc.Iterable): - return ",".join(safe_strseq_iter(val)) - return str(val) diff --git a/tests/unit/helpers/gcs/test_collections.py b/tests/unit/helpers/gcs/test_collections.py index f90ef8bb7..cde074a7a 100644 --- a/tests/unit/helpers/gcs/test_collections.py +++ b/tests/unit/helpers/gcs/test_collections.py @@ -13,8 +13,8 @@ POSIXStagingCollectionPolicies, ) from globus_sdk._missing import MISSING, MissingType, filter_missing +from globus_sdk._types import UUIDLike from globus_sdk.transport import JSONRequestEncoder -from globus_sdk.utils import UUIDLike STUB_SG_ID = uuid.uuid1() # storage gateway STUB_MC_ID = uuid.uuid1() # mapped collection diff --git a/tests/unit/test_remarshal.py b/tests/unit/test_remarshal.py new file mode 100644 index 000000000..5f4b97f7a --- /dev/null +++ b/tests/unit/test_remarshal.py @@ -0,0 +1,57 @@ +import collections.abc +import uuid + +import pytest + +from globus_sdk import MISSING +from globus_sdk._remarshal import commajoin, safe_strseq_iter, safe_strseq_listify + + +@pytest.mark.parametrize( + "value, expected_result", + ( + ("foo", ["foo"]), + ((1, 2, 3), ["1", "2", "3"]), + (uuid.UUID(int=10), [f"{uuid.UUID(int=10)}"]), + (["foo", uuid.UUID(int=5)], ["foo", f"{uuid.UUID(int=5)}"]), + ), +) +def test_safe_strseq_iter(value, expected_result): + iter_ = safe_strseq_iter(value) + assert not isinstance(iter_, list) + assert isinstance(iter_, collections.abc.Iterator) + assert list(iter_) == expected_result + assert list(iter_) == [] + + +@pytest.mark.parametrize( + "value, expected_result", + ( + ("foo", ["foo"]), + ((1, 2, 3), ["1", "2", "3"]), + (uuid.UUID(int=10), [f"{uuid.UUID(int=10)}"]), + (["foo", uuid.UUID(int=5)], ["foo", f"{uuid.UUID(int=5)}"]), + (MISSING, MISSING), + (None, None), + ), +) +def test_safe_strseq_listify(value, expected_result): + list_ = safe_strseq_listify(value) + assert isinstance(list_, list) or list_ in (MISSING, None) + assert list_ == expected_result + + +@pytest.mark.parametrize( + "value, expected_result", + ( + ("foo", "foo"), + (uuid.UUID(int=10), f"{uuid.UUID(int=10)}"), + ((1, 2, 3), "1,2,3"), + (range(5), "0,1,2,3,4"), + (["foo", uuid.UUID(int=5)], f"foo,{uuid.UUID(int=5)}"), + (MISSING, MISSING), + ), +) +def test_commajoin(value, expected_result): + joined = commajoin(value) + assert joined == expected_result diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4b0edd847..ad4d6631a 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,5 +1,3 @@ -import uuid - import pytest from globus_sdk import utils @@ -43,16 +41,3 @@ def test_slash_join(a, b): Confirms all have the same correct slash_join output """ assert utils.slash_join(a, b) == "a/b" - - -@pytest.mark.parametrize( - "value, expected_result", - ( - ("foo", ["foo"]), - ((1, 2, 3), ["1", "2", "3"]), - (uuid.UUID(int=10), [f"{uuid.UUID(int=10)}"]), - (["foo", uuid.UUID(int=5)], ["foo", f"{uuid.UUID(int=5)}"]), - ), -) -def test_safe_strseq_iter(value, expected_result): - assert list(utils.safe_strseq_iter(value)) == expected_result From fc4186f872254c44de3beaa956e92c027e30974f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 10 Jun 2025 16:06:23 -0500 Subject: [PATCH 038/176] Rename 'utils' to '_utils' And update import style to be consistently absolute FromImport. --- docs/core/utils.rst | 2 +- src/globus_sdk/_testing/models.py | 2 +- src/globus_sdk/{utils.py => _utils.py} | 0 src/globus_sdk/authorizers/access_token.py | 4 ++-- src/globus_sdk/authorizers/renewing.py | 5 +++-- src/globus_sdk/client.py | 5 +++-- .../login_flows/command_line_login_flow_manager.py | 2 +- .../local_server_login_flow_manager.py | 2 +- .../services/auth/flow_managers/authorization_code.py | 4 ++-- src/globus_sdk/services/auth/flow_managers/native_app.py | 6 +++--- src/globus_sdk/services/gcs/client.py | 5 +++-- src/globus_sdk/services/timers/data.py | 2 +- tests/common/globus_responses.py | 4 ++-- tests/functional/services/timers/test_jobs.py | 5 +++-- tests/unit/test_utils.py | 8 ++++---- 15 files changed, 30 insertions(+), 26 deletions(-) rename src/globus_sdk/{utils.py => _utils.py} (100%) diff --git a/docs/core/utils.rst b/docs/core/utils.rst index a83825745..568f09620 100644 --- a/docs/core/utils.rst +++ b/docs/core/utils.rst @@ -23,7 +23,7 @@ As a result, where ``MISSING`` is used as the default for a value, ``None`` can be used to explicitly pass the value ``null``. .. class:: globus_sdk.MissingType - :canonical: globus_sdk.utils.MissingType + :canonical: globus_sdk._missing.MissingType This is the type of ``MISSING``. diff --git a/src/globus_sdk/_testing/models.py b/src/globus_sdk/_testing/models.py index dec2c6d23..d5536153f 100644 --- a/src/globus_sdk/_testing/models.py +++ b/src/globus_sdk/_testing/models.py @@ -5,7 +5,7 @@ import responses -from ..utils import slash_join +from globus_sdk._utils import slash_join class RegisteredResponse: diff --git a/src/globus_sdk/utils.py b/src/globus_sdk/_utils.py similarity index 100% rename from src/globus_sdk/utils.py rename to src/globus_sdk/_utils.py diff --git a/src/globus_sdk/authorizers/access_token.py b/src/globus_sdk/authorizers/access_token.py index 6aafd64ef..def3a2ede 100644 --- a/src/globus_sdk/authorizers/access_token.py +++ b/src/globus_sdk/authorizers/access_token.py @@ -1,6 +1,6 @@ import logging -from globus_sdk import utils +from globus_sdk._utils import sha256_string from .base import StaticGlobusAuthorizer @@ -24,5 +24,5 @@ def __init__(self, access_token: str) -> None: self.access_token = access_token self.header_val = "Bearer %s" % access_token - self.access_token_hash = utils.sha256_string(self.access_token) + self.access_token_hash = sha256_string(self.access_token) log.debug(f'Bearer token has hash "{self.access_token_hash}"') diff --git a/src/globus_sdk/authorizers/renewing.py b/src/globus_sdk/authorizers/renewing.py index 16093db33..39215ae77 100644 --- a/src/globus_sdk/authorizers/renewing.py +++ b/src/globus_sdk/authorizers/renewing.py @@ -5,7 +5,8 @@ import time import typing as t -from globus_sdk import exc, utils +from globus_sdk import exc +from globus_sdk._utils import sha256_string from .base import GlobusAuthorizer @@ -97,7 +98,7 @@ def access_token(self) -> str | None: def access_token(self, value: str | None) -> None: self._access_token = value if value: - self._access_token_hash = utils.sha256_string(value) + self._access_token_hash = sha256_string(value) @abc.abstractmethod def _get_token_response(self) -> ResponseT: diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 9fd0918c6..1e1a19b79 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -4,9 +4,10 @@ import typing as t import urllib.parse -from globus_sdk import GlobusSDKUsageError, config, exc, utils +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 from globus_sdk.response import GlobusHTTPResponse @@ -477,7 +478,7 @@ def request( if path.startswith("https://") or path.startswith("http://"): url = path else: - url = utils.slash_join(self.base_url, urllib.parse.quote(path)) + url = slash_join(self.base_url, urllib.parse.quote(path)) # either use given authorizer or get one from app if automatic_authorization: diff --git a/src/globus_sdk/login_flows/command_line_login_flow_manager.py b/src/globus_sdk/login_flows/command_line_login_flow_manager.py index 26a2ae097..85fa92320 100644 --- a/src/globus_sdk/login_flows/command_line_login_flow_manager.py +++ b/src/globus_sdk/login_flows/command_line_login_flow_manager.py @@ -5,9 +5,9 @@ from contextlib import contextmanager import globus_sdk +from globus_sdk._utils import get_nice_hostname from globus_sdk.exc.base import GlobusError from globus_sdk.gare import GlobusAuthorizationParameters -from globus_sdk.utils import get_nice_hostname from .login_flow_manager import LoginFlowManager diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py index 0bee663ed..3849b4370 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py @@ -8,9 +8,9 @@ from string import Template import globus_sdk +from globus_sdk._utils import get_nice_hostname from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows.login_flow_manager import LoginFlowManager -from globus_sdk.utils import get_nice_hostname from .errors import LocalServerEnvironmentalLoginError, LocalServerLoginError from .local_server import DEFAULT_HTML_TEMPLATE, RedirectHandler, RedirectHTTPServer diff --git a/src/globus_sdk/services/auth/flow_managers/authorization_code.py b/src/globus_sdk/services/auth/flow_managers/authorization_code.py index 0a1cbac28..289028af2 100644 --- a/src/globus_sdk/services/auth/flow_managers/authorization_code.py +++ b/src/globus_sdk/services/auth/flow_managers/authorization_code.py @@ -4,8 +4,8 @@ import typing as t import urllib.parse -from globus_sdk import utils from globus_sdk._types import ScopeCollectionType +from globus_sdk._utils import slash_join from .._common import stringify_requested_scopes from ..response import OAuthAuthorizationCodeResponse @@ -86,7 +86,7 @@ def get_authorize_url(self, query_params: dict[str, t.Any] | None = None) -> str either to your provided ``redirect_uri`` or to the default location, with the ``auth_code`` embedded in a query parameter. """ - authorize_base_url = utils.slash_join( + authorize_base_url = slash_join( self.auth_client.base_url, "/v2/oauth2/authorize" ) log.debug(f"Building authorization URI. Base URL: {authorize_base_url}") diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index d4c21342e..06766a663 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -8,8 +8,8 @@ import typing as t import urllib.parse -from globus_sdk import utils from globus_sdk._types import ScopeCollectionType +from globus_sdk._utils import slash_join from globus_sdk.exc import GlobusSDKUsageError from .._common import stringify_requested_scopes @@ -127,7 +127,7 @@ def __init__( # default to `/v2/web/auth-code` on whatever environment we're looking # at -- most typically it will be `https://auth.globus.org/` self.redirect_uri = redirect_uri or ( - utils.slash_join(auth_client.base_url, "/v2/web/auth-code") + slash_join(auth_client.base_url, "/v2/web/auth-code") ) # make a challenge and secret to keep @@ -166,7 +166,7 @@ def get_authorize_url(self, query_params: dict[str, t.Any] | None = None) -> str either to your provided ``redirect_uri`` or to the default location, with the ``auth_code`` embedded in a query parameter. """ - authorize_base_url = utils.slash_join( + authorize_base_url = slash_join( self.auth_client.base_url, "/v2/oauth2/authorize" ) log.debug(f"Building authorization URI. Base URL: {authorize_base_url}") diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 31d89cebf..fb3449030 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -3,11 +3,12 @@ import typing as t import uuid -from globus_sdk import client, exc, paging, response, scopes, utils +from globus_sdk import client, exc, paging, response, scopes from globus_sdk._classproperty import classproperty from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike +from globus_sdk._utils import slash_join from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import Scope @@ -66,7 +67,7 @@ def __init__( # if it was an HTTPS URL, check that it ends with /api/ elif not gcs_address.endswith(("/api/", "/api")): # if it doesn't, add it - gcs_address = utils.slash_join(gcs_address, "/api/") + gcs_address = slash_join(gcs_address, "/api/") self._endpoint_client_id: str | None = None diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index 30a893ae6..fb86cc38b 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -8,10 +8,10 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import Payload +from globus_sdk._utils import slash_join from globus_sdk.config import get_service_url from globus_sdk.exc import warn_deprecated from globus_sdk.services.transfer import TransferData -from globus_sdk.utils import slash_join log = logging.getLogger(__name__) diff --git a/tests/common/globus_responses.py b/tests/common/globus_responses.py index 078211143..a3c67e1f5 100644 --- a/tests/common/globus_responses.py +++ b/tests/common/globus_responses.py @@ -3,7 +3,7 @@ import responses -from globus_sdk import utils +from globus_sdk._utils import slash_join def register_api_route_fixture_file(service, path, filename, **kwargs): @@ -54,7 +54,7 @@ def register_api_route( } assert service in base_url_map base_url = base_url_map.get(service) - full_url = utils.slash_join(base_url, path) + full_url = slash_join(base_url, path) # can set it to `{}` explicitly to clear the default if adding_headers is None: diff --git a/tests/functional/services/timers/test_jobs.py b/tests/functional/services/timers/test_jobs.py index ed84b4515..d988c2ef9 100644 --- a/tests/functional/services/timers/test_jobs.py +++ b/tests/functional/services/timers/test_jobs.py @@ -3,8 +3,9 @@ import pytest -from globus_sdk import TimerJob, TimersAPIError, TransferData, config, exc, utils +from globus_sdk import TimerJob, TimersAPIError, TransferData, config, exc from globus_sdk._testing import get_last_request, load_response +from globus_sdk._utils import slash_join from tests.common import GO_EP1_ID, GO_EP2_ID @@ -60,7 +61,7 @@ def test_create_job(client, start, interval): assert req_body["interval"] == interval.total_seconds() else: assert req_body["interval"] == interval - assert req_body["callback_url"] == utils.slash_join( + assert req_body["callback_url"] == slash_join( config.get_service_url("actions"), "/transfer/transfer/run" ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ad4d6631a..2e78a16f8 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,13 +1,13 @@ import pytest -from globus_sdk import utils +from globus_sdk._utils import get_nice_hostname, sha256_string, slash_join def test_sha256string(): test_string = "foo" expected_sha = "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" - assert utils.sha256_string(test_string) == expected_sha + assert sha256_string(test_string) == expected_sha @pytest.mark.parametrize( @@ -26,7 +26,7 @@ def test_sha256string(): ) def test_get_nice_hostname(platform_value, result, monkeypatch): monkeypatch.setattr("platform.node", lambda: platform_value) - assert utils.get_nice_hostname() == result + assert get_nice_hostname() == result @pytest.mark.parametrize( @@ -40,4 +40,4 @@ def test_slash_join(a, b): to b's with and without leading "/" Confirms all have the same correct slash_join output """ - assert utils.slash_join(a, b) == "a/b" + assert slash_join(a, b) == "a/b" From fd81e1929d64590a66ede7febe3166b6e4b80c9b Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 10 Jun 2025 22:33:59 -0500 Subject: [PATCH 039/176] Convert GCS payload types from "_set_opt..." These `_set_opt...` helpers on the Payload class are mostly unnecessary now that we have defaults of MISSING. They are the way that nontrivial conversions are managed, so these need to be replaced with use of helpers from `globus_sdk._remarshal`. To aid in this, two new helpers are added to `_remarshal`. This removes the last use of `Payload._set_optints`, and is therefore paired with removal of that helper. Likewise, `Payload._set_optstrlists` is also removed as it is no longer used. --- src/globus_sdk/_payload.py | 23 +-- src/globus_sdk/_remarshal.py | 53 +++++++ src/globus_sdk/services/gcs/data/role.py | 10 +- .../services/gcs/data/storage_gateway.py | 137 +++++++++--------- .../services/gcs/data/user_credential.py | 16 +- 5 files changed, 133 insertions(+), 106 deletions(-) diff --git a/src/globus_sdk/_payload.py b/src/globus_sdk/_payload.py index 0e8fd2e1a..23f3cda5b 100644 --- a/src/globus_sdk/_payload.py +++ b/src/globus_sdk/_payload.py @@ -4,8 +4,9 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import safe_strseq_iter +# TODO: Remove this dispatch after we drop Python 3.8 support. +# In 3.9+ `dict.__class_getitem__` is available. if t.TYPE_CHECKING: # pylint: disable=unsubscriptable-object _PayloadBaseDict = dict[str, t.Any] @@ -58,17 +59,6 @@ def _set_optstrs(self, **kwargs: t.Any) -> None: for k, v in kwargs.items(): self._set_value(k, v, callback=str) - def _set_optstrlists( - self, **kwargs: t.Iterable[t.Any] | None | MissingType - ) -> None: - """ - Convenience function for setting a collection of omittable string list values. - - Values are converted to lists of strings prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=lambda x: list(safe_strseq_iter(x))) - def _set_optbools(self, **kwargs: bool | None | MissingType) -> None: """ Convenience function for setting a collection of omittable bool values. @@ -78,15 +68,6 @@ def _set_optbools(self, **kwargs: bool | None | MissingType) -> None: for k, v in kwargs.items(): self._set_value(k, v, callback=bool) - def _set_optints(self, **kwargs: t.Any) -> None: - """ - Convenience function for setting a collection of omittable int values. - - Values are converted to ints prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=int) - class AbstractPayload(Payload, abc.ABC): """ diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py index a0bd104b5..ea5d133ab 100644 --- a/src/globus_sdk/_remarshal.py +++ b/src/globus_sdk/_remarshal.py @@ -15,6 +15,9 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike +T = t.TypeVar("T") +R = t.TypeVar("R") + def safe_strseq_iter( value: t.Iterable[t.Any] | str | uuid.UUID, @@ -68,6 +71,56 @@ def safe_strseq_listify( return list(safe_strseq_iter(value)) +@t.overload +def listify(value: None) -> None: ... +@t.overload +def listify(value: MissingType) -> MissingType: ... +@t.overload +def listify(value: t.Iterable[T]) -> list[T]: ... + + +def listify(value: t.Iterable[T] | MissingType | None) -> list[T] | MissingType | None: + """ + Convert any iterable to a list, with handling for None and Missing. + """ + if value is None: + return None + if isinstance(value, MissingType): + return MISSING + if isinstance(value, list): + return value + return list(value) + + +@t.overload +def safe_list_map(value: None, mapped_function: t.Callable[[T], R]) -> None: ... + + +@t.overload +def safe_list_map( + value: MissingType, mapped_function: t.Callable[[T], R] +) -> MissingType: ... + + +@t.overload +def safe_list_map( + value: t.Iterable[T], mapped_function: t.Callable[[T], R] +) -> list[R]: ... + + +def safe_list_map( + value: t.Iterable[T] | MissingType | None, mapped_function: t.Callable[[T], R] +) -> list[R] | MissingType | None: + """ + Like map() but handles None|MISSING and listifies the result otherwise. + """ + if value is None: + return None + if isinstance(value, MissingType): + return MISSING + return [mapped_function(element) for element in value] + + @t.overload def commajoin(value: MissingType) -> MissingType: ... @t.overload diff --git a/src/globus_sdk/services/gcs/data/role.py b/src/globus_sdk/services/gcs/data/role.py index 2371f8020..cc878f2ee 100644 --- a/src/globus_sdk/services/gcs/data/role.py +++ b/src/globus_sdk/services/gcs/data/role.py @@ -31,10 +31,8 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - collection=collection, - principal=principal, - role=role, - ) + self["DATA_TYPE"] = DATA_TYPE + self["collection"] = collection + self["principal"] = principal + self["role"] = role self.update(additional_fields or {}) diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index c8344fb7e..8ad01e22d 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -1,10 +1,12 @@ from __future__ import annotations import abc +import copy import typing as t from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import Payload +from globus_sdk._remarshal import listify, safe_list_map, safe_strseq_listify from globus_sdk._types import UUIDLike from ._common import DatatypeCallback, ensure_datatype @@ -72,21 +74,18 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - display_name=display_name, - connector_id=connector_id, - root=root, - ) - self._set_optstrlists( - allowed_domains=allowed_domains, - users_allow=users_allow, - users_deny=users_deny, - ) - self._set_optbools(high_assurance=high_assurance, require_mfa=require_mfa) - self._set_optints(authentication_timeout_mins=authentication_timeout_mins) - self._set_value("identity_mappings", identity_mappings, callback=list) - self._set_value("policies", policies) + self["DATA_TYPE"] = DATA_TYPE + self["display_name"] = display_name + self["connector_id"] = connector_id + self["root"] = root + self["allowed_domains"] = safe_strseq_listify(allowed_domains) + self["users_allow"] = safe_strseq_listify(users_allow) + self["users_deny"] = safe_strseq_listify(users_deny) + self["high_assurance"] = high_assurance + self["require_mfa"] = require_mfa + self["authentication_timeout_mins"] = authentication_timeout_mins + self["identity_mappings"] = listify(identity_mappings) + self["policies"] = policies self.update(additional_fields or {}) ensure_datatype(self) @@ -123,8 +122,9 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE) - self._set_optstrlists(groups_allow=groups_allow, groups_deny=groups_deny) + self["DATA_TYPE"] = DATA_TYPE + self["groups_allow"] = safe_strseq_listify(groups_allow) + self["groups_deny"] = safe_strseq_listify(groups_deny) self.update(additional_fields or {}) @@ -155,13 +155,12 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE, stage_app=stage_app) - self._set_optstrlists(groups_allow=groups_allow, groups_deny=groups_deny) - self._set_value( - "environment", - environment, - callback=lambda env_iter: [{**e} for e in env_iter], - ) + self["DATA_TYPE"] = DATA_TYPE + self["stage_app"] = stage_app + self["groups_allow"] = safe_strseq_listify(groups_allow) + self["groups_deny"] = safe_strseq_listify(groups_deny) + # make shallow copies of all the dicts passed + self["environment"] = safe_list_map(environment, copy.copy) self.update(additional_fields or {}) @@ -188,11 +187,9 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - s3_endpoint=s3_endpoint, - bp_access_id_file=bp_access_id_file, - ) + self["DATA_TYPE"] = DATA_TYPE + self["s3_endpoint"] = s3_endpoint + self["bp_access_id_file"] = bp_access_id_file self.update(additional_fields or {}) @@ -218,8 +215,9 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE, enterpriseID=enterpriseID) - self._set_value("boxAppSettings", boxAppSettings) + self["DATA_TYPE"] = DATA_TYPE + self["enterpriseID"] = enterpriseID + self["boxAppSettings"] = boxAppSettings self.update(additional_fields or {}) @@ -250,13 +248,11 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - s3_endpoint=s3_endpoint, - ceph_admin_key_id=ceph_admin_key_id, - ceph_admin_secret_key=ceph_admin_secret_key, - ) - self._set_optstrlists(s3_buckets=s3_buckets) + self["DATA_TYPE"] = DATA_TYPE + self["s3_endpoint"] = s3_endpoint + self["ceph_admin_key_id"] = ceph_admin_key_id + self["ceph_admin_secret_key"] = ceph_admin_secret_key + self["s3_buckets"] = safe_strseq_listify(s3_buckets) self.update(additional_fields or {}) @@ -284,8 +280,10 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE, client_id=client_id, secret=secret) - self._set_optints(user_api_rate_quota=user_api_rate_quota) + self["DATA_TYPE"] = DATA_TYPE + self["client_id"] = client_id + self["secret"] = secret + self["user_api_rate_quota"] = user_api_rate_quota self.update(additional_fields or {}) @@ -325,9 +323,12 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE, client_id=client_id, secret=secret) - self._set_optstrlists(buckets=buckets, projects=projects) - self._set_value("service_account_key", service_account_key) + self["DATA_TYPE"] = DATA_TYPE + self["client_id"] = client_id + self["secret"] = secret + self["buckets"] = safe_strseq_listify(buckets) + self["projects"] = safe_strseq_listify(projects) + self["service_account_key"] = service_account_key self.update(additional_fields or {}) @@ -357,10 +358,11 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, client_id=client_id, secret=secret, tenant=tenant - ) - self._set_optints(user_api_rate_limit=user_api_rate_limit) + self["DATA_TYPE"] = DATA_TYPE + self["client_id"] = client_id + self["secret"] = secret + self["tenant"] = tenant + self["user_api_rate_limit"] = user_api_rate_limit self.update(additional_fields or {}) @@ -394,15 +396,13 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - client_id=client_id, - secret=secret, - tenant=tenant, - account=account, - auth_type=auth_type, - ) - self._set_optbools(adls=adls) + self["DATA_TYPE"] = DATA_TYPE + self["client_id"] = client_id + self["secret"] = secret + self["tenant"] = tenant + self["account"] = account + self["auth_type"] = auth_type + self["adls"] = adls self.update(additional_fields or {}) @@ -432,9 +432,10 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs(DATA_TYPE=DATA_TYPE, s3_endpoint=s3_endpoint) - self._set_optbools(s3_user_credential_required=s3_user_credential_required) - self._set_optstrlists(s3_buckets=s3_buckets) + self["DATA_TYPE"] = DATA_TYPE + self["s3_endpoint"] = s3_endpoint + self["s3_user_credential_required"] = s3_user_credential_required + self["s3_buckets"] = safe_strseq_listify(s3_buckets) self.update(additional_fields or {}) @@ -465,11 +466,9 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - irods_environment_file=irods_environment_file, - irods_authentication_file=irods_authentication_file, - ) + self["DATA_TYPE"] = DATA_TYPE + self["irods_environment_file"] = irods_environment_file + self["irods_authentication_file"] = irods_authentication_file self.update(additional_fields or {}) @@ -496,10 +495,8 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - authentication_mech=authentication_mech, - authenticator=authenticator, - ) - self._set_optbools(uda_checksum_support=uda_checksum_support) + self["DATA_TYPE"] = DATA_TYPE + self["authentication_mech"] = authentication_mech + self["authenticator"] = authenticator + self["uda_checksum_support"] = uda_checksum_support self.update(additional_fields or {}) diff --git a/src/globus_sdk/services/gcs/data/user_credential.py b/src/globus_sdk/services/gcs/data/user_credential.py index ecfef3d5e..cda0557c5 100644 --- a/src/globus_sdk/services/gcs/data/user_credential.py +++ b/src/globus_sdk/services/gcs/data/user_credential.py @@ -37,13 +37,11 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - self._set_optstrs( - DATA_TYPE=DATA_TYPE, - identity_id=identity_id, - connector_id=connector_id, - username=username, - display_name=display_name, - storage_gateway_id=storage_gateway_id, - ) - self._set_value("policies", policies) + self["DATA_TYPE"] = DATA_TYPE + self["identity_id"] = identity_id + self["connector_id"] = connector_id + self["username"] = username + self["display_name"] = display_name + self["storage_gateway_id"] = storage_gateway_id + self["policies"] = policies self.update(additional_fields or {}) From 9641db4247dcef3fbf7ac835e1301356df55468d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 11 Jun 2025 00:59:34 -0500 Subject: [PATCH 040/176] Convert Transfer payload types from _set_opt... As with other payload types, convert away from the `_set_optbools`, `_set_optstrs`, and `_set_value` helpers. Use functions from `_remarshal` instead. Remove the now-unused methods of `Payload`. (Payload now defines no methods.) --- src/globus_sdk/_payload.py | 45 ---------------- src/globus_sdk/_remarshal.py | 13 +++++ src/globus_sdk/services/timers/data.py | 2 +- .../services/transfer/data/delete_data.py | 34 +++++------- .../services/transfer/data/transfer_data.py | 53 ++++++++----------- tests/unit/helpers/test_transfer.py | 10 ++-- 6 files changed, 56 insertions(+), 101 deletions(-) diff --git a/src/globus_sdk/_payload.py b/src/globus_sdk/_payload.py index 23f3cda5b..9d1bca502 100644 --- a/src/globus_sdk/_payload.py +++ b/src/globus_sdk/_payload.py @@ -3,8 +3,6 @@ import abc import typing as t -from globus_sdk._missing import MISSING, MissingType - # TODO: Remove this dispatch after we drop Python 3.8 support. # In 3.9+ `dict.__class_getitem__` is available. if t.TYPE_CHECKING: @@ -25,49 +23,6 @@ class Payload(_PayloadBaseDict): the requested encoder (e.g. as a JSON request body). """ - # - # internal helpers for setting non-null values - # - - def _set_value( - self, - key: str, - val: t.Any, - callback: t.Callable[[t.Any], t.Any] | None = None, - ) -> None: - """ - Internal helper for setting an omittable value on the payload. - - If the value is non-None, it will be set and the callback (if provided) will be - invoked on it. - Otherwise, it will be ignored and the callback will not be invoked. - - :param key: The key to set. - :param val: The value to set. - :param callback: An optional callback to apply to the value immediately - before it is set. - """ - if val is not None and val is not MISSING: - self[key] = callback(val) if callback else val - - def _set_optstrs(self, **kwargs: t.Any) -> None: - """ - Convenience function for setting a collection of omittable string values. - - Values are converted to strings prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=str) - - def _set_optbools(self, **kwargs: bool | None | MissingType) -> None: - """ - Convenience function for setting a collection of omittable bool values. - - Values are converted to bools prior to assignment. - """ - for k, v in kwargs.items(): - self._set_value(k, v, callback=bool) - class AbstractPayload(Payload, abc.ABC): """ diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py index ea5d133ab..39a0a9911 100644 --- a/src/globus_sdk/_remarshal.py +++ b/src/globus_sdk/_remarshal.py @@ -45,6 +45,19 @@ def safe_strseq_iter( yield str(x) +def safe_stringify( + value: object | MissingType | None, +) -> str | MissingType | None: + """ + Given any object or a MISSING|None, return `str(object) | MISSING | None`. + """ + if value is None: + return None + if isinstance(value, MissingType): + return MISSING + return str(value) + + @t.overload def safe_strseq_listify(value: None) -> None: ... @t.overload diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index fb86cc38b..9ae5fb6bf 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -312,7 +312,7 @@ def from_transfer_data( "Creating TimerJob from TransferData, action_url=%s", transfer_action_url ) for key in ("submission_id", "skip_activation_check"): - if key in transfer_data: + if transfer_data.get(key, MISSING) is not MISSING: raise ValueError( f"cannot create TimerJob from TransferData which has {key} set" ) diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index da93eb4fc..d44ec139a 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -7,6 +7,7 @@ from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import Payload +from globus_sdk._remarshal import safe_stringify from globus_sdk._types import UUIDLike if t.TYPE_CHECKING: @@ -112,27 +113,20 @@ def __init__( self["DATA_TYPE"] = "delete" self["DATA"] = [] - self._set_optstrs( - endpoint=endpoint, - label=label, - submission_id=submission_id - or ( - transfer_client.get_submission_id()["value"] - if transfer_client - else None - ), - deadline=deadline, - local_user=local_user, - ) - self._set_optbools( - recursive=recursive, - ignore_missing=ignore_missing, - interpret_globs=interpret_globs, - skip_activation_check=skip_activation_check, - notify_on_succeeded=notify_on_succeeded, - notify_on_failed=notify_on_failed, - notify_on_inactive=notify_on_inactive, + self["endpoint"] = endpoint + self["label"] = label + self["submission_id"] = submission_id or ( + transfer_client.get_submission_id()["value"] if transfer_client else MISSING ) + self["deadline"] = safe_stringify(deadline) + self["local_user"] = local_user + self["recursive"] = recursive + self["ignore_missing"] = ignore_missing + self["interpret_globs"] = interpret_globs + self["skip_activation_check"] = skip_activation_check + self["notify_on_succeeded"] = notify_on_succeeded + self["notify_on_failed"] = notify_on_failed + self["notify_on_inactive"] = notify_on_inactive for k, v in self.items(): log.debug("DeleteData.%s = %s", k, v) diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index 7ab25bd0a..cbf81001b 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -22,8 +22,8 @@ def _parse_sync_level( - sync_level: t.Literal["exists", "size", "mtime", "checksum"] | int, -) -> int: + sync_level: t.Literal["exists", "size", "mtime", "checksum"] | int | MissingType, +) -> int | MissingType: """ Map sync_level strings to known int values @@ -201,34 +201,27 @@ def __init__( log.debug("Creating a new TransferData object") self["DATA_TYPE"] = "transfer" self["DATA"] = [] - self._set_optstrs( - source_endpoint=source_endpoint, - destination_endpoint=destination_endpoint, - label=label, - submission_id=submission_id - or ( - transfer_client.get_submission_id()["value"] - if transfer_client - else None - ), - recursive_symlinks=recursive_symlinks, - deadline=deadline, - source_local_user=source_local_user, - destination_local_user=destination_local_user, + self["source_endpoint"] = source_endpoint + self["destination_endpoint"] = destination_endpoint + self["label"] = label + self["submission_id"] = submission_id or ( + transfer_client.get_submission_id()["value"] if transfer_client else MISSING ) - self._set_optbools( - verify_checksum=verify_checksum, - preserve_timestamp=preserve_timestamp, - encrypt_data=encrypt_data, - skip_activation_check=skip_activation_check, - skip_source_errors=skip_source_errors, - fail_on_quota_errors=fail_on_quota_errors, - delete_destination_extra=delete_destination_extra, - notify_on_succeeded=notify_on_succeeded, - notify_on_failed=notify_on_failed, - notify_on_inactive=notify_on_inactive, - ) - self._set_value("sync_level", sync_level, callback=_parse_sync_level) + self["recursive_symlinks"] = recursive_symlinks + self["deadline"] = deadline + self["source_local_user"] = source_local_user + self["destination_local_user"] = destination_local_user + self["verify_checksum"] = verify_checksum + self["preserve_timestamp"] = preserve_timestamp + self["encrypt_data"] = encrypt_data + self["skip_activation_check"] = skip_activation_check + self["skip_source_errors"] = skip_source_errors + self["fail_on_quota_errors"] = fail_on_quota_errors + self["delete_destination_extra"] = delete_destination_extra + self["notify_on_succeeded"] = notify_on_succeeded + self["notify_on_failed"] = notify_on_failed + self["notify_on_inactive"] = notify_on_inactive + self["sync_level"] = _parse_sync_level(sync_level) for k, v in self.items(): log.debug("TransferData.%s = %s", k, v) @@ -374,7 +367,7 @@ def add_filter_rule( ``tdata`` now describes a transfer which will only transfer files with the ``.txt`` extension. """ - if "filter_rules" not in self: + if self.get("filter_rules", MISSING) is MISSING: self["filter_rules"] = [] rule = { "DATA_TYPE": "filter_rule", diff --git a/tests/unit/helpers/test_transfer.py b/tests/unit/helpers/test_transfer.py index 9d23aef2b..75dc857c2 100644 --- a/tests/unit/helpers/test_transfer.py +++ b/tests/unit/helpers/test_transfer.py @@ -61,7 +61,7 @@ def test_transfer_init_no_client(): assert tdata["DATA_TYPE"] == "transfer" assert tdata["source_endpoint"] == GO_EP1_ID assert tdata["destination_endpoint"] == GO_EP2_ID - assert "submission_id" not in tdata + assert tdata["submission_id"] is MISSING assert "DATA" in tdata assert len(tdata["DATA"]) == 0 @@ -204,7 +204,7 @@ def test_delete_init_no_client(args, kwargs): ddata = DeleteData(*args, **kwargs) assert ddata["DATA_TYPE"] == "delete" assert ddata["endpoint"] == GO_EP1_ID - assert "submission_id" not in ddata + assert ddata["submission_id"] is MISSING assert "DATA" in ddata assert len(ddata["DATA"]) == 0 @@ -312,8 +312,8 @@ def _default(x): assert tdata[k] is v assert ddata[k] is v else: - assert k not in tdata - assert k not in ddata + assert tdata[k] is MISSING + assert ddata[k] is MISSING @pytest.mark.parametrize( @@ -362,7 +362,7 @@ def create(**kwargs): if value is None: # not present if not provided as a param or provided as explicit None - assert "skip_activation_check" not in create() + assert create()["skip_activation_check"] is MISSING elif value: data = create(skip_activation_check=True) assert "skip_activation_check" in data From 93b593a5e956cf58d0a44769ee1b4f31c66b2bc1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 11 Jun 2025 01:37:58 -0500 Subject: [PATCH 041/176] Add a changelog for cleanup to 'utils' --- changelog.d/20250611_012221_sirosen_utils_cleanup.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog.d/20250611_012221_sirosen_utils_cleanup.rst diff --git a/changelog.d/20250611_012221_sirosen_utils_cleanup.rst b/changelog.d/20250611_012221_sirosen_utils_cleanup.rst new file mode 100644 index 000000000..0ca670d38 --- /dev/null +++ b/changelog.d/20250611_012221_sirosen_utils_cleanup.rst @@ -0,0 +1,6 @@ +Changed +~~~~~~~ + +- Payload types now inherit from ``dict`` rather than ``UserDict``. The + ``PayloadWrapper`` utility class has been replaced with ``Payload``. (:pr:`NUMBER`) +- Payload types are more consistent about encoding missing values using ``MISSING``. (:pr:`NUMBER`) From 95951ab002ddbc4705c8d7b9f1d5b2cc26204830 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 11 Jun 2025 02:19:14 -0500 Subject: [PATCH 042/176] Linting fixes + adjust PR number in changelog --- .../20250611_012221_sirosen_utils_cleanup.rst | 6 ++++-- src/globus_sdk/_payload.py | 10 ++++++++-- src/globus_sdk/_remarshal.py | 13 ++++++++++--- src/globus_sdk/client.py | 8 +++++++- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/changelog.d/20250611_012221_sirosen_utils_cleanup.rst b/changelog.d/20250611_012221_sirosen_utils_cleanup.rst index 0ca670d38..7b456b041 100644 --- a/changelog.d/20250611_012221_sirosen_utils_cleanup.rst +++ b/changelog.d/20250611_012221_sirosen_utils_cleanup.rst @@ -2,5 +2,7 @@ Changed ~~~~~~~ - Payload types now inherit from ``dict`` rather than ``UserDict``. The - ``PayloadWrapper`` utility class has been replaced with ``Payload``. (:pr:`NUMBER`) -- Payload types are more consistent about encoding missing values using ``MISSING``. (:pr:`NUMBER`) + ``PayloadWrapper`` utility class has been replaced with ``Payload``. + (:pr:`1222`) +- Payload types are more consistent about encoding missing values using ``MISSING``. + (:pr:`1222`) diff --git a/src/globus_sdk/_payload.py b/src/globus_sdk/_payload.py index 9d1bca502..bb8307ec9 100644 --- a/src/globus_sdk/_payload.py +++ b/src/globus_sdk/_payload.py @@ -1,13 +1,19 @@ from __future__ import annotations import abc +import sys import typing as t +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + # TODO: Remove this dispatch after we drop Python 3.8 support. # In 3.9+ `dict.__class_getitem__` is available. if t.TYPE_CHECKING: # pylint: disable=unsubscriptable-object - _PayloadBaseDict = dict[str, t.Any] + _PayloadBaseDict = t.Dict[str, t.Any] else: _PayloadBaseDict = dict @@ -37,7 +43,7 @@ class AbstractPayload(Payload, abc.ABC): # explicitly define `__new__` in order to check for abstract methods which # were not redefined - def __new__(cls, *args: t.Any, **kwargs: t.Any) -> t.Self: + def __new__(cls, *args: t.Any, **kwargs: t.Any) -> Self: obj = super().__new__(cls, *args, **kwargs) abstractmethods: frozenset[str] = ( obj.__abstractmethods__ # type: ignore[attr-defined] diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py index 39a0a9911..f70dac69b 100644 --- a/src/globus_sdk/_remarshal.py +++ b/src/globus_sdk/_remarshal.py @@ -45,11 +45,11 @@ def safe_strseq_iter( yield str(x) -def safe_stringify( - value: object | MissingType | None, -) -> str | MissingType | None: +def safe_stringify(value: object | MissingType | None) -> str | MissingType | None: """ Given any object or a MISSING|None, return `str(object) | MISSING | None`. + + :param value: The stringifiable object """ if value is None: return None @@ -76,6 +76,8 @@ def safe_strseq_listify( Unlike safe_strseq_iter, this may be the "last mile" remarshalling step before data is actually passed to the network layer. Therefore, it makes sense for this helper to handle (MISSING | None). + + :param value: The stringifiable object or iterable of objects """ if value is None: return None @@ -95,6 +97,8 @@ def listify(value: t.Iterable[T]) -> list[T]: ... def listify(value: t.Iterable[T] | MissingType | None) -> list[T] | MissingType | None: """ Convert any iterable to a list, with handling for None and Missing. + + :param value: The iterable of objects """ if value is None: return None @@ -126,6 +130,9 @@ def safe_list_map( ) -> list[R] | MissingType | None: """ Like map() but handles None|MISSING and listifies the result otherwise. + + :param value: The iterable of objects over which to map + :param mapped_function: The function to map """ if value is None: return None diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 1e1a19b79..ee937d67c 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import sys import typing as t import urllib.parse @@ -14,12 +15,17 @@ from globus_sdk.scopes import Scope, ScopeBuilder from globus_sdk.transport import RequestsTransport +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + if t.TYPE_CHECKING: from globus_sdk.globus_app import GlobusApp log = logging.getLogger(__name__) -_DataParamType: t.TypeAlias = t.Union[None, str, bytes, t.Dict[str, t.Any]] +_DataParamType: TypeAlias = t.Union[None, str, bytes, t.Dict[str, t.Any]] class BaseClient: From ec6692fc320e35b36d3a9ac0d282699580b94d98 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 11 Jun 2025 02:25:15 -0500 Subject: [PATCH 043/176] Fix lazy import test --- .../lazy-imports/test_modules_do_not_require_requests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index 681cf6339..d7a97dc2b 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -35,10 +35,13 @@ # internal components and utilities are a special case: # failing to ensure that these avoid 'requests' can make it more difficult # to ensure that the main parts (above) do not transitively pick it up + "_classproperty", "_guards", + "_missing", + "_remarshal", "_serializable", "_types", - "utils", + "_utils", ), ) def test_module_does_not_require_requests(module_name): From c911038ec730f46cb25013b160c11854c2192369 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 13 Jun 2025 16:17:07 -0500 Subject: [PATCH 044/176] Apply suggestions from code review Co-authored-by: derek-globus <113056046+derek-globus@users.noreply.github.com> --- src/globus_sdk/_classproperty.py | 10 +++------- src/globus_sdk/_remarshal.py | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/globus_sdk/_classproperty.py b/src/globus_sdk/_classproperty.py index bdb1944d3..d23162b14 100644 --- a/src/globus_sdk/_classproperty.py +++ b/src/globus_sdk/_classproperty.py @@ -29,14 +29,10 @@ def _in_sphinx_build() -> bool: # pragma: no cover class _classproperty(t.Generic[T, R]): """ - This is a well-typed Generic Descriptor which can be used to wrap decorated - functions. + A hybrid class/instance property descriptor. - Note that this descriptor will pass an instance (self) if possible, and the - class (cls) only if there is no instance. This is unlike ``classmethod``. - - For more guidance on how this works, see the python3 descriptor guide: - https://docs.python.org/3/howto/descriptor.html#properties + On a class, the decorated method will be invoked with `cls`. + On an instance, the decorated method will be invoked with `self`. """ def __init__(self, func: t.Callable[[type[T] | T], R]) -> None: diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py index f70dac69b..9974cc93c 100644 --- a/src/globus_sdk/_remarshal.py +++ b/src/globus_sdk/_remarshal.py @@ -23,7 +23,7 @@ def safe_strseq_iter( value: t.Iterable[t.Any] | str | uuid.UUID, ) -> t.Iterator[str]: """ - Given an Iterable (typically of strings), produce an iterator over it of strings. + Iterate over one or more string/string-convertible values. :param value: The stringifiable object or objects to iterate over @@ -47,7 +47,7 @@ def safe_strseq_iter( def safe_stringify(value: object | MissingType | None) -> str | MissingType | None: """ - Given any object or a MISSING|None, return `str(object) | MISSING | None`. + Convert a value to a string, with handling for None and Missing. :param value: The stringifiable object """ From be863815a64cfb492dd3bba041258e53dc2feeb4 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 17 Jun 2025 08:53:17 -0500 Subject: [PATCH 045/176] Rename "Payload" -> "GlobusPayload" --- src/globus_sdk/_payload.py | 6 +++--- src/globus_sdk/services/auth/data.py | 4 ++-- src/globus_sdk/services/compute/data.py | 6 +++--- src/globus_sdk/services/flows/data.py | 4 ++-- src/globus_sdk/services/gcs/data/collection.py | 6 +++--- src/globus_sdk/services/gcs/data/endpoint.py | 4 ++-- src/globus_sdk/services/gcs/data/role.py | 4 ++-- src/globus_sdk/services/gcs/data/storage_gateway.py | 7 +++---- src/globus_sdk/services/gcs/data/user_credential.py | 4 ++-- src/globus_sdk/services/groups/data.py | 6 +++--- src/globus_sdk/services/search/data.py | 6 +++--- src/globus_sdk/services/timers/data.py | 10 +++++----- src/globus_sdk/services/transfer/data/delete_data.py | 4 ++-- src/globus_sdk/services/transfer/data/transfer_data.py | 4 ++-- tests/unit/test_payload.py | 8 ++++---- tests/unit/transport/test_transport_encoders.py | 8 ++++---- 16 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/globus_sdk/_payload.py b/src/globus_sdk/_payload.py index bb8307ec9..3e52857f0 100644 --- a/src/globus_sdk/_payload.py +++ b/src/globus_sdk/_payload.py @@ -18,7 +18,7 @@ _PayloadBaseDict = dict -class Payload(_PayloadBaseDict): +class GlobusPayload(_PayloadBaseDict): """ A class for defining helper objects which wrap some kind of "payload" dict. Typical for helper objects which formulate a request payload. @@ -30,9 +30,9 @@ class Payload(_PayloadBaseDict): """ -class AbstractPayload(Payload, abc.ABC): +class AbstractGlobusPayload(GlobusPayload, abc.ABC): """ - An abstract class which is a Payload. + An abstract class which is a GlobusPayload. This is a shim which is needed because we have a metaclass conflict between dict:type and ABC:ABCMeta. diff --git a/src/globus_sdk/services/auth/data.py b/src/globus_sdk/services/auth/data.py index af4057d56..21c496cb0 100644 --- a/src/globus_sdk/services/auth/data.py +++ b/src/globus_sdk/services/auth/data.py @@ -1,8 +1,8 @@ -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._types import UUIDLike -class DependentScopeSpec(Payload): +class DependentScopeSpec(GlobusPayload): """ Utility class for creating dependent scope values as parameters to :meth:`AuthClient.create_scope ` diff --git a/src/globus_sdk/services/compute/data.py b/src/globus_sdk/services/compute/data.py index fedfac2e6..7ccd7557d 100644 --- a/src/globus_sdk/services/compute/data.py +++ b/src/globus_sdk/services/compute/data.py @@ -1,12 +1,12 @@ from __future__ import annotations from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._types import UUIDLike from globus_sdk.exc import warn_deprecated -class ComputeFunctionMetadata(Payload): +class ComputeFunctionMetadata(GlobusPayload): """ .. warning:: @@ -30,7 +30,7 @@ def __init__( self["sdk_version"] = sdk_version -class ComputeFunctionDocument(Payload): +class ComputeFunctionDocument(GlobusPayload): """ .. warning:: diff --git a/src/globus_sdk/services/flows/data.py b/src/globus_sdk/services/flows/data.py index 9a44c0917..3f128a221 100644 --- a/src/globus_sdk/services/flows/data.py +++ b/src/globus_sdk/services/flows/data.py @@ -4,12 +4,12 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload log = logging.getLogger(__name__) -class RunActivityNotificationPolicy(Payload): +class RunActivityNotificationPolicy(GlobusPayload): """ A notification policy for a run, determining when emails will be sent. diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index 627bb3178..529511b74 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -4,7 +4,7 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import AbstractPayload +from globus_sdk._payload import AbstractGlobusPayload from globus_sdk._remarshal import safe_strseq_listify from globus_sdk._types import UUIDLike @@ -61,7 +61,7 @@ def _user_message_length_callback( # Declare a metaclass of ABCMeta even though inheriting from `Payload` renders it # inert. This will let type checkers understand that this class is abstract. -class CollectionDocument(AbstractPayload): +class CollectionDocument(AbstractGlobusPayload): """ This is the base class for :class:`~.MappedCollectionDocument` and :class:`~.GuestCollectionDocument`. @@ -487,7 +487,7 @@ def __init__( ensure_datatype(self) -class CollectionPolicies(AbstractPayload): +class CollectionPolicies(AbstractGlobusPayload): """ This is the abstract base type for Collection Policies documents to use as the ``policies`` parameter when creating a MappedCollectionDocument. diff --git a/src/globus_sdk/services/gcs/data/endpoint.py b/src/globus_sdk/services/gcs/data/endpoint.py index 5a7fc234a..748d22072 100644 --- a/src/globus_sdk/services/gcs/data/endpoint.py +++ b/src/globus_sdk/services/gcs/data/endpoint.py @@ -3,12 +3,12 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._remarshal import safe_strseq_listify from globus_sdk.services.gcs.data._common import DatatypeCallback, ensure_datatype -class EndpointDocument(Payload): +class EndpointDocument(GlobusPayload): r""" :param data_type: Explicitly set the ``DATA_TYPE`` value for this endpoint document. diff --git a/src/globus_sdk/services/gcs/data/role.py b/src/globus_sdk/services/gcs/data/role.py index cc878f2ee..b1ce6ca5e 100644 --- a/src/globus_sdk/services/gcs/data/role.py +++ b/src/globus_sdk/services/gcs/data/role.py @@ -3,11 +3,11 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._types import UUIDLike -class GCSRoleDocument(Payload): +class GCSRoleDocument(GlobusPayload): """ Convenience class for constructing a Role document to use as the `data` parameter to `create_role` diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index 8ad01e22d..74ccd1c56 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -1,18 +1,17 @@ from __future__ import annotations -import abc import copy import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import AbstractGlobusPayload, GlobusPayload from globus_sdk._remarshal import listify, safe_list_map, safe_strseq_listify from globus_sdk._types import UUIDLike from ._common import DatatypeCallback, ensure_datatype -class StorageGatewayDocument(Payload): +class StorageGatewayDocument(GlobusPayload): """ Convenience class for constructing a Storage Gateway document to use as the `data` parameter to ``create_storage_gateway`` or @@ -90,7 +89,7 @@ def __init__( ensure_datatype(self) -class StorageGatewayPolicies(Payload, abc.ABC): +class StorageGatewayPolicies(AbstractGlobusPayload): """ This is the abstract base type for Storage Policies documents to use as the ``policies`` parameter when creating a StorageGatewayDocument. diff --git a/src/globus_sdk/services/gcs/data/user_credential.py b/src/globus_sdk/services/gcs/data/user_credential.py index cda0557c5..d625ada90 100644 --- a/src/globus_sdk/services/gcs/data/user_credential.py +++ b/src/globus_sdk/services/gcs/data/user_credential.py @@ -3,11 +3,11 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._types import UUIDLike -class UserCredentialDocument(Payload): +class UserCredentialDocument(GlobusPayload): """ Convenience class for constructing a UserCredential document to use as the `data` parameter to `create_user_credential` and diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index f884c41a7..e351014a4 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -4,7 +4,7 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._remarshal import safe_strseq_iter from globus_sdk._types import UUIDLike @@ -99,7 +99,7 @@ def _docstring_fixer(cls: type[T]) -> type[T]: return cls -class BatchMembershipActions(Payload): +class BatchMembershipActions(GlobusPayload): """ An object used to represent a batch action on memberships of a group. `Perform actions on group members @@ -256,7 +256,7 @@ def request_join( @_docstring_fixer -class GroupPolicies(Payload): +class GroupPolicies(GlobusPayload): """ An object used to represent the policy settings of a group. This may be used to set or modify group settings. diff --git a/src/globus_sdk/services/search/data.py b/src/globus_sdk/services/search/data.py index 72baa0b88..2a23636b7 100644 --- a/src/globus_sdk/services/search/data.py +++ b/src/globus_sdk/services/search/data.py @@ -4,7 +4,7 @@ from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload # workaround for absence of Self type # for the workaround and some background, see: @@ -22,7 +22,7 @@ def _format_histogram_range( # an internal class for declaring multiple related types with shared methods -class SearchQueryBase(Payload): +class SearchQueryBase(GlobusPayload): """ The base class for all Search query helpers. @@ -227,7 +227,7 @@ def add_sort( return self -class SearchQueryV1(Payload): +class SearchQueryV1(GlobusPayload): """ A specialized dict which has helpers for creating and modifying a Search Query document. Replaces the usage of ``SearchQuery``. diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index 9ae5fb6bf..09258f29c 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -7,7 +7,7 @@ import typing as t from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._utils import slash_join from globus_sdk.config import get_service_url from globus_sdk.exc import warn_deprecated @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) -class TransferTimer(Payload): +class TransferTimer(GlobusPayload): """ A helper for defining a payload for Transfer Timer creation. Use this along with :meth:`create_timer ` to @@ -125,7 +125,7 @@ def _preprocess_body( return new_body -class RecurringTimerSchedule(Payload): +class RecurringTimerSchedule(GlobusPayload): """ A helper used as part of a *timer* to define when the *timer* will run. @@ -183,7 +183,7 @@ def __init__( } -class OnceTimerSchedule(Payload): +class OnceTimerSchedule(GlobusPayload): """ A helper used as part of a *timer* to define when the *timer* will run. @@ -203,7 +203,7 @@ def __init__( self["datetime"] = _format_date(datetime) -class TimerJob(Payload): +class TimerJob(GlobusPayload): r""" .. warning:: diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index d44ec139a..d9a06de4d 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -6,7 +6,7 @@ from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._remarshal import safe_stringify from globus_sdk._types import UUIDLike @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) -class DeleteData(Payload): +class DeleteData(GlobusPayload): r""" Convenience class for constructing a delete document, to use as the `data` parameter to diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index cbf81001b..a654ff088 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -6,7 +6,7 @@ from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk._types import UUIDLike if t.TYPE_CHECKING: @@ -37,7 +37,7 @@ def _parse_sync_level( return sync_level -class TransferData(Payload): +class TransferData(GlobusPayload): r""" Convenience class for constructing a transfer document, to use as the ``data`` parameter to diff --git a/tests/unit/test_payload.py b/tests/unit/test_payload.py index ded48d6ee..d1adf9c13 100644 --- a/tests/unit/test_payload.py +++ b/tests/unit/test_payload.py @@ -2,12 +2,12 @@ import pytest -from globus_sdk._payload import AbstractPayload, Payload +from globus_sdk._payload import AbstractGlobusPayload, GlobusPayload def test_payload_methods(): # just make sure that PayloadWrapper acts like a dict... - data = Payload() + data = GlobusPayload() assert "foo" not in data with pytest.raises(KeyError): data["foo"] @@ -28,12 +28,12 @@ def test_payload_methods(): def test_abstract_payload_detects_abstract_methods(): # A has no abstract methods so it will instantiate - class A(AbstractPayload): + class A(AbstractGlobusPayload): pass A() - # B has an abstract method and inherits from AbstractPayload so it should + # B has an abstract method and inherits from AbstractGlobusPayload so it should # fail to instantiate class B(A): @abc.abstractmethod diff --git a/tests/unit/transport/test_transport_encoders.py b/tests/unit/transport/test_transport_encoders.py index 351e39398..559227b9b 100644 --- a/tests/unit/transport/test_transport_encoders.py +++ b/tests/unit/transport/test_transport_encoders.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import MISSING -from globus_sdk._payload import Payload +from globus_sdk._payload import GlobusPayload from globus_sdk.transport import FormRequestEncoder, JSONRequestEncoder, RequestEncoder @@ -85,7 +85,7 @@ def test_all_request_encoders_remove_missing_in_params_and_headers(encoder_class # nested payload wrappers (get dictified / "unwrapped") ( True, - {"bar": Payload(foo=1), "baz": [2, Payload(foo=1)]}, + {"bar": GlobusPayload(foo=1), "baz": [2, GlobusPayload(foo=1)]}, {"bar": {"foo": 1}, "baz": [2, {"foo": 1}]}, ), # document with UUIDs and tuples buried inside nested structures @@ -100,7 +100,7 @@ def test_json_encoder_payload_preparation( using_payload_type, payload_contents, expected_data ): encoder = JSONRequestEncoder() - x = Payload() if using_payload_type else {} + x = GlobusPayload() if using_payload_type else {} for k, v in payload_contents.items(): x[k] = v request = encoder.encode( @@ -148,7 +148,7 @@ def test_form_encoder_payload_preparation( using_payload_type, payload_contents, expected_data ): encoder = FormRequestEncoder() - x = Payload() if using_payload_type else {} + x = GlobusPayload() if using_payload_type else {} for k, v in payload_contents.items(): x[k] = v request = encoder.encode( From 92fe40a3afa372a497eca8de33ab7ecfc0b0d933 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 17 Jun 2025 08:55:55 -0500 Subject: [PATCH 046/176] Remove imprecise use of 'Any' in _remarshal These `Iterable[Any]` containers were added as a low-impact conversion from `Iterable` when we set `--disallow-any-generics` for `mypy`. Their true type has always been `str|UUID`. --- src/globus_sdk/_remarshal.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py index 9974cc93c..6d9cdbcf3 100644 --- a/src/globus_sdk/_remarshal.py +++ b/src/globus_sdk/_remarshal.py @@ -20,7 +20,7 @@ def safe_strseq_iter( - value: t.Iterable[t.Any] | str | uuid.UUID, + value: t.Iterable[str | uuid.UUID] | str | uuid.UUID, ) -> t.Iterator[str]: """ Iterate over one or more string/string-convertible values. @@ -62,12 +62,16 @@ def safe_stringify(value: object | MissingType | None) -> str | MissingType | No def safe_strseq_listify(value: None) -> None: ... @t.overload def safe_strseq_listify(value: MissingType) -> MissingType: ... + + @t.overload -def safe_strseq_listify(value: t.Iterable[t.Any] | str | uuid.UUID) -> list[str]: ... +def safe_strseq_listify( + value: t.Iterable[str | uuid.UUID] | str | uuid.UUID, +) -> list[str]: ... def safe_strseq_listify( - value: t.Iterable[t.Any] | str | uuid.UUID | MissingType | None, + value: t.Iterable[str | uuid.UUID] | str | uuid.UUID | MissingType | None, ) -> list[str] | MissingType | None: """ A wrapper over safe_strseq_iter which produces list outputs. From 3e13d3ad776c8956d72d0f3a13cd3934fab92ff7 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 17 Jun 2025 11:17:54 -0500 Subject: [PATCH 047/176] Refine the internal naming of remarshal functions 1. Introduce `Omittable` and `NullableOmittable` type aliases to better describe the types of our transformations 2. Remove `safe_` as a prefix from all names. It originally was used to indicate safety of iteration over strings or collections of strings, but grew unclear in scope as the word was applied to other contexts. 3. Improve the internal documentation around string safety contracts. 4. Expand `commajoin` to handle `None`, making it more uniform with the other helpers. 5. Remove use of `UUIDLike` as an alias in the remarshalling helpers. 6. Update all usage sites, primarily removing `safe_` as a prefix from function calls. --- src/globus_sdk/_remarshal.py | 159 ++++++++++-------- .../auth/client/confidential_client.py | 6 +- .../services/auth/client/service_client.py | 10 +- src/globus_sdk/services/compute/client.py | 4 +- .../services/gcs/data/collection.py | 16 +- src/globus_sdk/services/gcs/data/endpoint.py | 4 +- .../services/gcs/data/storage_gateway.py | 26 +-- src/globus_sdk/services/groups/data.py | 30 ++-- src/globus_sdk/services/search/client.py | 4 +- .../services/transfer/data/delete_data.py | 4 +- tests/unit/test_remarshal.py | 48 +++++- 11 files changed, 183 insertions(+), 128 deletions(-) diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py index 6d9cdbcf3..e35a43c8a 100644 --- a/src/globus_sdk/_remarshal.py +++ b/src/globus_sdk/_remarshal.py @@ -9,17 +9,66 @@ from __future__ import annotations import collections.abc +import sys import typing as t import uuid from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike + +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias T = t.TypeVar("T") R = t.TypeVar("R") +# Omittable[T] means "T may be missing" +# NullableOmittable[T] means "T may be missing or null" +# +# in type systems this kind of construction is sometimes called "Optional" or "Maybe" +# but Python uses "Optional" to mean "T | None" and in the SDK, "None" means "null" +Omittable: TypeAlias[T] = t.Union[T, MissingType] +NullableOmittable: TypeAlias[T] = t.Union[Omittable[T], None] + + +def stringify(value: NullableOmittable[object]) -> NullableOmittable[str]: + """ + Convert a value to a string, with handling for None and Missing. + + :param value: The stringifiable object + """ + if value is None: + return None + if isinstance(value, MissingType): + return MISSING + return str(value) + + +@t.overload +def listify(value: None) -> None: ... +@t.overload +def listify(value: MissingType) -> MissingType: ... +@t.overload +def listify(value: t.Iterable[T]) -> list[T]: ... + + +def listify(value: NullableOmittable[t.Iterable[T]]) -> NullableOmittable[list[T]]: + """ + Convert any iterable to a list, with handling for None and Missing. + + :param value: The iterable of objects + """ + if value is None: + return None + if isinstance(value, MissingType): + return MISSING + if isinstance(value, list): + return value + return list(value) + -def safe_strseq_iter( +def strseq_iter( value: t.Iterable[str | uuid.UUID] | str | uuid.UUID, ) -> t.Iterator[str]: """ @@ -27,10 +76,20 @@ def safe_strseq_iter( :param value: The stringifiable object or objects to iterate over - This is a passthrough with some caveats: - - if the value is a solitary string, yield only that value - - if the value is a solitary UUID, yield only that value (as a string) - - str values in the iterable which are not strings + This function handles strings, which are themselves iterable, + by producing the string itself, not it's characters: + + >>> list("foo") + ['f', 'o', 'o'] + >>> list(strseq_iter("foo")) + ['foo'] + + It also accepts and converts UUIDs and iterables thereof: + + >>> list(strseq_iter(UUID(int=0))) + ['00000000-0000-0000-0000-000000000000'] + >>> list(strseq_iter([UUID(int=0), UUID(int=1)])) + ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'] This helps handle cases where a string is passed to a function expecting an iterable of strings, as well as cases where an iterable of UUID objects is accepted for a @@ -45,39 +104,26 @@ def safe_strseq_iter( yield str(x) -def safe_stringify(value: object | MissingType | None) -> str | MissingType | None: - """ - Convert a value to a string, with handling for None and Missing. - - :param value: The stringifiable object - """ - if value is None: - return None - if isinstance(value, MissingType): - return MISSING - return str(value) - - @t.overload -def safe_strseq_listify(value: None) -> None: ... +def strseq_listify(value: None) -> None: ... @t.overload -def safe_strseq_listify(value: MissingType) -> MissingType: ... +def strseq_listify(value: MissingType) -> MissingType: ... @t.overload -def safe_strseq_listify( +def strseq_listify( value: t.Iterable[str | uuid.UUID] | str | uuid.UUID, ) -> list[str]: ... -def safe_strseq_listify( - value: t.Iterable[str | uuid.UUID] | str | uuid.UUID | MissingType | None, -) -> list[str] | MissingType | None: +def strseq_listify( + value: NullableOmittable[t.Iterable[str | uuid.UUID] | str | uuid.UUID], +) -> NullableOmittable[list[str]]: """ - A wrapper over safe_strseq_iter which produces list outputs. + A wrapper over strseq_iter which produces list outputs. This method takes responsibility for checking for MISSING and None values. - Unlike safe_strseq_iter, this may be the "last mile" remarshalling step before + Unlike strseq_iter, this may be the "last mile" remarshalling step before data is actually passed to the network layer. Therefore, it makes sense for this helper to handle (MISSING | None). @@ -87,53 +133,28 @@ def safe_strseq_listify( return None if isinstance(value, MissingType): return MISSING - return list(safe_strseq_iter(value)) - - -@t.overload -def listify(value: None) -> None: ... -@t.overload -def listify(value: MissingType) -> MissingType: ... -@t.overload -def listify(value: t.Iterable[T]) -> list[T]: ... - - -def listify(value: t.Iterable[T] | MissingType | None) -> list[T] | MissingType | None: - """ - Convert any iterable to a list, with handling for None and Missing. - - :param value: The iterable of objects - """ - if value is None: - return None - if isinstance(value, MissingType): - return MISSING - if isinstance(value, list): - return value - return list(value) + return list(strseq_iter(value)) @t.overload -def safe_list_map(value: None, mapped_function: t.Callable[[T], R]) -> None: ... +def list_map(value: None, mapped_function: t.Callable[[T], R]) -> None: ... @t.overload -def safe_list_map( +def list_map( value: MissingType, mapped_function: t.Callable[[T], R] ) -> MissingType: ... @t.overload -def safe_list_map( - value: t.Iterable[T], mapped_function: t.Callable[[T], R] -) -> list[R]: ... +def list_map(value: t.Iterable[T], mapped_function: t.Callable[[T], R]) -> list[R]: ... -def safe_list_map( - value: t.Iterable[T] | MissingType | None, mapped_function: t.Callable[[T], R] -) -> list[R] | MissingType | None: +def list_map( + value: NullableOmittable[t.Iterable[T]], mapped_function: t.Callable[[T], R] +) -> NullableOmittable[list[R]]: """ - Like map() but handles None|MISSING and listifies the result otherwise. + Like list(map()) but handles None|MISSING. :param value: The iterable of objects over which to map :param mapped_function: The function to map @@ -148,16 +169,20 @@ def safe_list_map( @t.overload def commajoin(value: MissingType) -> MissingType: ... @t.overload -def commajoin(value: UUIDLike | t.Iterable[UUIDLike]) -> str: ... +def commajoin(value: None) -> None: ... +@t.overload +def commajoin(value: str | uuid.UUID | t.Iterable[str | uuid.UUID]) -> str: ... def commajoin( - value: UUIDLike | t.Iterable[UUIDLike] | MissingType, -) -> str | MissingType: - # note that this explicit handling of Iterable allows for string-like objects to be - # passed to this function and be stringified by the `str()` call + value: NullableOmittable[str | uuid.UUID | t.Iterable[str | uuid.UUID]], +) -> NullableOmittable[str]: + if value is None: + return None if isinstance(value, MissingType): - return value + return MISSING + # note that this explicit handling of Iterable allows for objects to be + # passed to this function and be stringified by the `str()` call if isinstance(value, collections.abc.Iterable): - return ",".join(safe_strseq_iter(value)) + return ",".join(strseq_iter(value)) return str(value) diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index a2c08b7b5..ce5072fda 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -5,7 +5,7 @@ from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin, safe_strseq_iter, safe_strseq_listify +from globus_sdk._remarshal import commajoin, strseq_iter, strseq_listify from globus_sdk._types import ScopeCollectionType, UUIDLike from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -271,7 +271,7 @@ def oauth2_get_dependent_tokens( if refresh_tokens: form_data["access_type"] = "offline" if not isinstance(scope, MissingType): - form_data["scope"] = " ".join(safe_strseq_iter(scope)) + form_data["scope"] = " ".join(strseq_iter(scope)) if additional_params: form_data.update(additional_params) @@ -457,7 +457,7 @@ def create_child_client( "client_type": client_type, } if not isinstance(redirect_uris, MissingType): - body["redirect_uris"] = safe_strseq_listify(redirect_uris) + body["redirect_uris"] = strseq_listify(redirect_uris) # terms_and_conditions and privacy_policy must both be set or unset if bool(terms_and_conditions) ^ bool(privacy_policy): diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 4e9940f47..aa941b798 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -8,7 +8,7 @@ from globus_sdk import client, exc from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin, safe_strseq_listify +from globus_sdk._remarshal import commajoin, strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.response import GlobusHTTPResponse, IterableResponse @@ -624,9 +624,9 @@ def create_project( "contact_email": contact_email, } if admin_ids is not None: - body["admin_ids"] = safe_strseq_listify(admin_ids) + body["admin_ids"] = strseq_listify(admin_ids) if admin_group_ids is not None: - body["admin_group_ids"] = safe_strseq_listify(admin_group_ids) + body["admin_group_ids"] = strseq_listify(admin_group_ids) return self.post("/v2/api/projects", data={"project": body}) def update_project( @@ -680,9 +680,9 @@ def update_project( if contact_email is not None: body["contact_email"] = contact_email if admin_ids is not None: - body["admin_ids"] = safe_strseq_listify(admin_ids) + body["admin_ids"] = strseq_listify(admin_ids) if admin_group_ids is not None: - body["admin_group_ids"] = safe_strseq_listify(admin_group_ids) + body["admin_group_ids"] = strseq_listify(admin_group_ids) return self.put(f"/v2/api/projects/{project_id}", data={"project": body}) def delete_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index abd7c2f49..374d84e2d 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -5,7 +5,7 @@ from globus_sdk import GlobusHTTPResponse, client from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import safe_strseq_listify +from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.scopes import ComputeScopes, Scope @@ -225,7 +225,7 @@ def get_task_batch( :ref: Root/operation/get_batch_status_v2_batch_status_post """ return self.post( - "/v2/batch_status", data={"task_ids": safe_strseq_listify(task_ids)} + "/v2/batch_status", data={"task_ids": strseq_listify(task_ids)} ) def get_task_group(self, task_group_id: UUIDLike) -> GlobusHTTPResponse: diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index 529511b74..5018ed16d 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -5,7 +5,7 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload -from globus_sdk._remarshal import safe_strseq_listify +from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from ._common import ( @@ -198,7 +198,7 @@ def __init__( ) self["user_message"] = user_message self["user_message_link"] = user_message_link - self["keywords"] = safe_strseq_listify(keywords) + self["keywords"] = strseq_listify(keywords) self["disable_verify"] = disable_verify self["enable_https"] = enable_https self["force_encryption"] = force_encryption @@ -357,8 +357,8 @@ def __init__( self["guest_auth_policy_id"] = guest_auth_policy_id self["storage_gateway_id"] = storage_gateway_id - self["sharing_users_allow"] = safe_strseq_listify(sharing_users_allow) - self["sharing_users_deny"] = safe_strseq_listify(sharing_users_deny) + self["sharing_users_allow"] = strseq_listify(sharing_users_allow) + self["sharing_users_deny"] = strseq_listify(sharing_users_deny) self["delete_protected"] = delete_protected self["allow_guest_collections"] = allow_guest_collections @@ -519,8 +519,8 @@ def __init__( super().__init__() self["DATA_TYPE"] = DATA_TYPE - self["sharing_groups_allow"] = safe_strseq_listify(sharing_groups_allow) - self["sharing_groups_deny"] = safe_strseq_listify(sharing_groups_deny) + self["sharing_groups_allow"] = strseq_listify(sharing_groups_allow) + self["sharing_groups_deny"] = strseq_listify(sharing_groups_deny) if not isinstance(additional_fields, MissingType): self.update(additional_fields) @@ -550,8 +550,8 @@ def __init__( ) -> None: super().__init__() self["DATA_TYPE"] = DATA_TYPE - self["sharing_groups_allow"] = safe_strseq_listify(sharing_groups_allow) - self["sharing_groups_deny"] = safe_strseq_listify(sharing_groups_deny) + self["sharing_groups_allow"] = strseq_listify(sharing_groups_allow) + self["sharing_groups_deny"] = strseq_listify(sharing_groups_deny) if not isinstance(additional_fields, MissingType): self.update(additional_fields) diff --git a/src/globus_sdk/services/gcs/data/endpoint.py b/src/globus_sdk/services/gcs/data/endpoint.py index 748d22072..cde6b3b63 100644 --- a/src/globus_sdk/services/gcs/data/endpoint.py +++ b/src/globus_sdk/services/gcs/data/endpoint.py @@ -4,7 +4,7 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._remarshal import safe_strseq_listify +from globus_sdk._remarshal import strseq_listify from globus_sdk.services.gcs.data._common import DatatypeCallback, ensure_datatype @@ -127,7 +127,7 @@ def __init__( self["info_link"] = info_link self["network_use"] = network_use self["organization"] = organization - self["keywords"] = safe_strseq_listify(keywords) + self["keywords"] = strseq_listify(keywords) self["allow_udt"] = allow_udt self["public"] = public self["max_concurrency"] = max_concurrency diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index 74ccd1c56..1cdbb4197 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -5,7 +5,7 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload, GlobusPayload -from globus_sdk._remarshal import listify, safe_list_map, safe_strseq_listify +from globus_sdk._remarshal import list_map, listify, strseq_listify from globus_sdk._types import UUIDLike from ._common import DatatypeCallback, ensure_datatype @@ -77,9 +77,9 @@ def __init__( self["display_name"] = display_name self["connector_id"] = connector_id self["root"] = root - self["allowed_domains"] = safe_strseq_listify(allowed_domains) - self["users_allow"] = safe_strseq_listify(users_allow) - self["users_deny"] = safe_strseq_listify(users_deny) + self["allowed_domains"] = strseq_listify(allowed_domains) + self["users_allow"] = strseq_listify(users_allow) + self["users_deny"] = strseq_listify(users_deny) self["high_assurance"] = high_assurance self["require_mfa"] = require_mfa self["authentication_timeout_mins"] = authentication_timeout_mins @@ -122,8 +122,8 @@ def __init__( ) -> None: super().__init__() self["DATA_TYPE"] = DATA_TYPE - self["groups_allow"] = safe_strseq_listify(groups_allow) - self["groups_deny"] = safe_strseq_listify(groups_deny) + self["groups_allow"] = strseq_listify(groups_allow) + self["groups_deny"] = strseq_listify(groups_deny) self.update(additional_fields or {}) @@ -156,10 +156,10 @@ def __init__( super().__init__() self["DATA_TYPE"] = DATA_TYPE self["stage_app"] = stage_app - self["groups_allow"] = safe_strseq_listify(groups_allow) - self["groups_deny"] = safe_strseq_listify(groups_deny) + self["groups_allow"] = strseq_listify(groups_allow) + self["groups_deny"] = strseq_listify(groups_deny) # make shallow copies of all the dicts passed - self["environment"] = safe_list_map(environment, copy.copy) + self["environment"] = list_map(environment, copy.copy) self.update(additional_fields or {}) @@ -251,7 +251,7 @@ def __init__( self["s3_endpoint"] = s3_endpoint self["ceph_admin_key_id"] = ceph_admin_key_id self["ceph_admin_secret_key"] = ceph_admin_secret_key - self["s3_buckets"] = safe_strseq_listify(s3_buckets) + self["s3_buckets"] = strseq_listify(s3_buckets) self.update(additional_fields or {}) @@ -325,8 +325,8 @@ def __init__( self["DATA_TYPE"] = DATA_TYPE self["client_id"] = client_id self["secret"] = secret - self["buckets"] = safe_strseq_listify(buckets) - self["projects"] = safe_strseq_listify(projects) + self["buckets"] = strseq_listify(buckets) + self["projects"] = strseq_listify(projects) self["service_account_key"] = service_account_key self.update(additional_fields or {}) @@ -434,7 +434,7 @@ def __init__( self["DATA_TYPE"] = DATA_TYPE self["s3_endpoint"] = s3_endpoint self["s3_user_credential_required"] = s3_user_credential_required - self["s3_buckets"] = safe_strseq_listify(s3_buckets) + self["s3_buckets"] = strseq_listify(s3_buckets) self.update(additional_fields or {}) diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index e351014a4..cafe6251b 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -5,7 +5,7 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._remarshal import safe_strseq_iter +from globus_sdk._remarshal import strseq_iter from globus_sdk._types import UUIDLike T = t.TypeVar("T") @@ -116,8 +116,7 @@ def accept_invites( :param identity_ids: The identities for whom to accept invites """ self.setdefault("accept", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self @@ -135,7 +134,7 @@ def add_members( """ self.setdefault("add", []).extend( {"identity_id": identity_id, "role": role} - for identity_id in safe_strseq_iter(identity_ids) + for identity_id in strseq_iter(identity_ids) ) return self @@ -148,8 +147,7 @@ def approve_pending( :param identity_ids: The identities to approve as members of the group """ self.setdefault("approve", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self @@ -162,8 +160,7 @@ def decline_invites( :param identity_ids: The identities for whom invitations should be declined """ self.setdefault("decline", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self @@ -181,7 +178,7 @@ def invite_members( """ self.setdefault("invite", []).extend( {"identity_id": identity_id, "role": role} - for identity_id in safe_strseq_iter(identity_ids) + for identity_id in strseq_iter(identity_ids) ) return self @@ -193,8 +190,7 @@ def join(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: :param identity_ids: The identities to use to join the group """ self.setdefault("join", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self @@ -206,8 +202,7 @@ def leave(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: :param identity_ids: The identities to remove from the group """ self.setdefault("leave", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self @@ -220,8 +215,7 @@ def reject_join_requests( :param identity_ids: The identities to reject from the group """ self.setdefault("reject", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self @@ -235,8 +229,7 @@ def remove_members( :param identity_ids: The identities to remove from the group """ self.setdefault("remove", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self @@ -249,8 +242,7 @@ def request_join( :param identity_ids: The identities to use to request membership in the group """ self.setdefault("request_join", []).extend( - {"identity_id": identity_id} - for identity_id in safe_strseq_iter(identity_ids) + {"identity_id": identity_id} for identity_id in strseq_iter(identity_ids) ) return self diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index 466c321e3..864048dbd 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -5,7 +5,7 @@ from globus_sdk import client, paging, response from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import safe_strseq_listify +from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import Scope, SearchScopes @@ -576,7 +576,7 @@ def batch_delete_by_subject( # ensure that a single string is *not* treated as an iterable of strings, # which is usually not intentional body = { - "subjects": safe_strseq_listify(subjects), + "subjects": strseq_listify(subjects), **(additional_params or {}), } return self.post(f"/v1/index/{index_id}/batch_delete_by_subject", data=body) diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index d9a06de4d..beaddde44 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -7,7 +7,7 @@ from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._remarshal import safe_stringify +from globus_sdk._remarshal import stringify from globus_sdk._types import UUIDLike if t.TYPE_CHECKING: @@ -118,7 +118,7 @@ def __init__( self["submission_id"] = submission_id or ( transfer_client.get_submission_id()["value"] if transfer_client else MISSING ) - self["deadline"] = safe_stringify(deadline) + self["deadline"] = stringify(deadline) self["local_user"] = local_user self["recursive"] = recursive self["ignore_missing"] = ignore_missing diff --git a/tests/unit/test_remarshal.py b/tests/unit/test_remarshal.py index 5f4b97f7a..b12a2a432 100644 --- a/tests/unit/test_remarshal.py +++ b/tests/unit/test_remarshal.py @@ -4,7 +4,13 @@ import pytest from globus_sdk import MISSING -from globus_sdk._remarshal import commajoin, safe_strseq_iter, safe_strseq_listify +from globus_sdk._remarshal import ( + commajoin, + list_map, + listify, + strseq_iter, + strseq_listify, +) @pytest.mark.parametrize( @@ -16,8 +22,8 @@ (["foo", uuid.UUID(int=5)], ["foo", f"{uuid.UUID(int=5)}"]), ), ) -def test_safe_strseq_iter(value, expected_result): - iter_ = safe_strseq_iter(value) +def test_strseq_iter(value, expected_result): + iter_ = strseq_iter(value) assert not isinstance(iter_, list) assert isinstance(iter_, collections.abc.Iterator) assert list(iter_) == expected_result @@ -35,8 +41,8 @@ def test_safe_strseq_iter(value, expected_result): (None, None), ), ) -def test_safe_strseq_listify(value, expected_result): - list_ = safe_strseq_listify(value) +def test_strseq_listify(value, expected_result): + list_ = strseq_listify(value) assert isinstance(list_, list) or list_ in (MISSING, None) assert list_ == expected_result @@ -50,8 +56,40 @@ def test_safe_strseq_listify(value, expected_result): (range(5), "0,1,2,3,4"), (["foo", uuid.UUID(int=5)], f"foo,{uuid.UUID(int=5)}"), (MISSING, MISSING), + (None, None), ), ) def test_commajoin(value, expected_result): joined = commajoin(value) assert joined == expected_result + + +@pytest.mark.parametrize( + "value, expected_result", + ( + ("foo", ["f", "o", "o"]), + ((1, 2, 3), [1, 2, 3]), + (range(5), [0, 1, 2, 3, 4]), + (["foo", uuid.UUID(int=5)], ["foo", uuid.UUID(int=5)]), + (MISSING, MISSING), + (None, None), + ), +) +def test_listify(value, expected_result): + converted = listify(value) + assert converted == expected_result + + +@pytest.mark.parametrize( + "value, expected_result", + ( + ("foo", ["ff", "oo", "oo"]), + ((1, 2, 3), [2, 4, 6]), + (range(5), [0, 2, 4, 6, 8]), + (MISSING, MISSING), + (None, None), + ), +) +def test_list_map(value, expected_result): + converted = list_map(value, lambda x: x * 2) + assert converted == expected_result From 4167548f3be8b828fe89f88bae7a0b1bcef8dd91 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Mon, 23 Jun 2025 10:20:23 -0500 Subject: [PATCH 048/176] Convert the CHANGELOG to Markdown-compatible headers (v4 branch) --- ...osen_add_specific_flow_data_access_scope_helper.rst | 2 +- ...36_sirosen_remove_deprecated_scope_parser_alias.rst | 2 +- ...7_max.tuecke_sc_15807_transfer_missing_defaults.rst | 4 ++-- changelog.d/20250611_012221_sirosen_utils_cleanup.rst | 2 +- changelog.rst | 10 +++++----- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst b/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst index c706ae634..20f439e07 100644 --- a/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst +++ b/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst @@ -1,5 +1,5 @@ Added -~~~~~ +----- - ``SpecificFlowClient`` has a new method, ``add_app_transfer_data_access_scope`` which facilitates declaration of scope diff --git a/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst b/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst index 97b06497c..78369f338 100644 --- a/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst +++ b/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst @@ -1,5 +1,5 @@ Removed -~~~~~~~ +------- - ``globus_sdk.experimental.scope_parser`` has been removed. Use ``globus_sdk.scopes`` instead. (:pr:`NUMBER`) diff --git a/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst b/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst index babe781a2..f27b81ad4 100644 --- a/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst +++ b/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst @@ -1,4 +1,4 @@ Breaking Changes -~~~~~~~~~~~~~~~~ +---------------- -- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Transfer client. (:pr:`1216`) \ No newline at end of file +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Transfer client. (:pr:`1216`) diff --git a/changelog.d/20250611_012221_sirosen_utils_cleanup.rst b/changelog.d/20250611_012221_sirosen_utils_cleanup.rst index 7b456b041..f036d583b 100644 --- a/changelog.d/20250611_012221_sirosen_utils_cleanup.rst +++ b/changelog.d/20250611_012221_sirosen_utils_cleanup.rst @@ -1,5 +1,5 @@ Changed -~~~~~~~ +------- - Payload types now inherit from ``dict`` rather than ``UserDict``. The ``PayloadWrapper`` utility class has been replaced with ``Payload``. diff --git a/changelog.rst b/changelog.rst index bfd667335..5900ed20c 100644 --- a/changelog.rst +++ b/changelog.rst @@ -15,10 +15,10 @@ to a major new version of the SDK. .. _changelog-4.0.0a2: v4.0.0a2 (2025-06-05) ---------------------- +===================== Breaking Changes -~~~~~~~~~~~~~~~~ +---------------- - The SDK version is no longer available in ``globus_sdk.version.__version__``. (:pr:`1195`) @@ -40,7 +40,7 @@ Breaking Changes (:pr:`1205`, :pr:`1207`, :pr:`1212`, :pr:`1214`) Removed -~~~~~~~ +------- - ``globus_sdk.experimental.auth_requirements_error`` has been removed. Use ``globus_sdk.gare`` instead. (:pr:`1202`) @@ -54,10 +54,10 @@ Removed .. _changelog-4.0.0a1: v4.0.0a1 (2025-05-20) ---------------------- +===================== Breaking Changes -~~~~~~~~~~~~~~~~ +---------------- - The SDK no longer sets default scopes for direct use of client credentials and auth client login flow methods. From 3360547e1c957af795edd805afcc2a6a6b3109d2 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 20 Jun 2025 10:58:02 -0500 Subject: [PATCH 049/176] Remove transfer_client param to data constructors Since v0.x of the SDK (specifically, since 2e78893de4 ), we have supported `transfer_client` as a parameter to `TransferData`, doing the `get_submission_id()` call at initialization time. In order to avoid a data constructor which accepts its relevant client, and the burden this can impose (e.g., in a multi-user app, keeping the two synced), we've been in transition to a model where the client is responsible for making this call and writing the value back into the payload at submission time. The `submit_transfer` and `submit_delete` methods already support fetching a submission ID on init, but they need a small tweak to allow for `MISSING` values to trigger that fetch. Docs and tests are updated, and a new section of the upgrading guide covers how to handle the change. --- ...5_sirosen_remove_transfer_client_param.rst | 6 + docs/upgrading.rst | 67 +++++++++ .../submit_transfer_detect_data_access.py | 5 +- .../scheduled_transfers/create_timer.py | 5 +- .../create_timer_detect_data_access.py | 5 +- .../submit_transfer_collections_known.py | 5 +- .../submit_transfer_collections_unknown.py | 5 +- .../transfer_relative_deadline/index.rst | 4 +- .../submit_transfer_relative_deadline.py | 4 +- src/globus_sdk/services/timers/client.py | 18 +-- src/globus_sdk/services/transfer/client.py | 15 +- .../services/transfer/data/delete_data.py | 32 +--- .../services/transfer/data/transfer_data.py | 33 +--- tests/functional/services/timers/test_jobs.py | 8 +- .../services/transfer/test_task_submit.py | 8 +- .../mypy-ignore-tests/transfer_data.py | 15 +- tests/unit/helpers/test_timer.py | 6 +- tests/unit/helpers/test_transfer.py | 141 ++++-------------- 18 files changed, 157 insertions(+), 225 deletions(-) create mode 100644 changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst diff --git a/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst b/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst new file mode 100644 index 000000000..3002f4297 --- /dev/null +++ b/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst @@ -0,0 +1,6 @@ +Breaking Changes +---------------- + +- Support for ``transfer_client`` as a parameter to ``TransferData`` and + ``DeleteData`` has been removed. See the upgrading doc for transition + details. (:pr:`NUMBER`) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index b555a9d12..110848750 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -45,6 +45,73 @@ Then, code can dispatch with From 3.x to 4.0 --------------- +``TransferData`` and ``DeleteData`` Do Not Take a ``TransferClient`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The signatures for these two data constructors has changed to remove support +for ``transfer_client`` as their first parameter. + +Generally, update usage which passed a client to omit it: + +.. code-block:: python + + from globus_sdk import TransferClient, TransferData, DeleteData + + # globus-sdk v3 + + tc = TransferClient(...) + tdata = TransferData(tc, SRC_COLLECTION, DST_COLLECTION) + tc.submit_transfer(tdata) + + tc = TransferClient(...) + ddata = DeleteData(tc, COLLECTION) + tc.submit_delete(tdata) + + # globus-sdk v4 + + tdata = TransferData(SRC_COLLECTION, DST_COLLECTION) + tc = TransferClient(...) + tc.submit_transfer(tdata) + + ddata = DeleteData(COLLECTION) + tc = TransferClient(...) + tc.submit_delete(tdata) + +Users who are using keyword arguments to pass collection IDs without a +``transfer_client`` do not need to make any change. For example: + +.. code-block:: python + + from globus_sdk import TransferData, DeleteData + + # globus-sdk v3 or v4 + + tdata = TransferData( + source_endpoint=SRC_COLLECTION, destination_endpoint=DST_COLLECTION + ) + ddata = DeleteData(endpoint=COLLECTION) + +The client object was used to fetch a ``submission_id`` on initialization. +Users typically will rely on ``TransferClient.submit_transfer()`` and +``TransferClient.submit_delete()`` filling in this value. +To control when a submission ID is fetched, use +``TransferClient.get_submsission_id()``, as in: + +.. code-block:: python + + from globus_sdk import TransferClient, TransferData + + # globus-sdk v3 or v4 + + tc = TransferClient(...) + submission_id = tc.get_submission_id()["value"] + + tdata = TransferData( + source_endpoint=SRC_COLLECTION, + destination_endpoint=DST_COLLECTION, + submission_id=submission_id, + ) + Deprecated Timers Aliases Removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user_guide/usage_patterns/data_transfer/detecting_data_access/submit_transfer_detect_data_access.py b/docs/user_guide/usage_patterns/data_transfer/detecting_data_access/submit_transfer_detect_data_access.py index 6a1a5eb7a..4347f52c0 100644 --- a/docs/user_guide/usage_patterns/data_transfer/detecting_data_access/submit_transfer_detect_data_access.py +++ b/docs/user_guide/usage_patterns/data_transfer/detecting_data_access/submit_transfer_detect_data_access.py @@ -42,10 +42,7 @@ def uses_data_access( if uses_data_access(transfer_client, DST_COLLECTION): transfer_client.add_app_data_access_scope(DST_COLLECTION) -transfer_request = globus_sdk.TransferData( - source_endpoint=SRC_COLLECTION, - destination_endpoint=DST_COLLECTION, -) +transfer_request = globus_sdk.TransferData(SRC_COLLECTION, DST_COLLECTION) transfer_request.add_item(SRC_PATH, DST_PATH) task = transfer_client.submit_transfer(transfer_request) diff --git a/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer.py b/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer.py index a101953f3..5191c3111 100644 --- a/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer.py +++ b/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer.py @@ -19,10 +19,7 @@ # as with an immediate data transfer, we take our input data and wrap them in # a TransferData object, representing the transfer task -transfer_request = globus_sdk.TransferData( - source_endpoint=SRC_COLLECTION, - destination_endpoint=DST_COLLECTION, -) +transfer_request = globus_sdk.TransferData(SRC_COLLECTION, DST_COLLECTION) transfer_request.add_item(SRC_PATH, DST_PATH) # we'll define the timer as one which runs every hour for 3 days diff --git a/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer_detect_data_access.py b/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer_detect_data_access.py index 5eeba25c2..fe076a9cd 100644 --- a/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer_detect_data_access.py +++ b/docs/user_guide/usage_patterns/data_transfer/scheduled_transfers/create_timer_detect_data_access.py @@ -41,10 +41,7 @@ def uses_data_access(collection_id: str) -> bool: # as with an immediate data transfer, we take our input data and wrap them in # a TransferData object, representing the transfer task -transfer_request = globus_sdk.TransferData( - source_endpoint=SRC_COLLECTION, - destination_endpoint=DST_COLLECTION, -) +transfer_request = globus_sdk.TransferData(SRC_COLLECTION, DST_COLLECTION) transfer_request.add_item(SRC_PATH, DST_PATH) # we'll define the timer as one which runs every hour for 3 days diff --git a/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_known.py b/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_known.py index 4cddca9b5..229da759f 100644 --- a/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_known.py +++ b/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_known.py @@ -24,10 +24,7 @@ def main(): transfer_client.add_app_data_access_scope(SRC_COLLECTION) transfer_client.add_app_data_access_scope(DST_COLLECTION) - transfer_request = globus_sdk.TransferData( - source_endpoint=SRC_COLLECTION, - destination_endpoint=DST_COLLECTION, - ) + transfer_request = globus_sdk.TransferData(SRC_COLLECTION, DST_COLLECTION) transfer_request.add_item(SRC_PATH, DST_PATH) task = transfer_client.submit_transfer(transfer_request) diff --git a/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_unknown.py b/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_unknown.py index 4c7bf1cba..3971736e9 100644 --- a/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_unknown.py +++ b/docs/user_guide/usage_patterns/data_transfer/submit_transfer/submit_transfer_collections_unknown.py @@ -19,10 +19,7 @@ def main(): transfer_client = globus_sdk.TransferClient(app=USER_APP) - transfer_request = globus_sdk.TransferData( - source_endpoint=SRC_COLLECTION, - destination_endpoint=DST_COLLECTION, - ) + transfer_request = globus_sdk.TransferData(SRC_COLLECTION, DST_COLLECTION) transfer_request.add_item(SRC_PATH, DST_PATH) try: diff --git a/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/index.rst b/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/index.rst index e6f99d853..31f2f63b6 100644 --- a/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/index.rst +++ b/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/index.rst @@ -79,8 +79,8 @@ a sample task document with a deadline set for "an hour from now": # create a Transfer Task request document, including a relative deadline transfer_request = globus_sdk.TransferData( - source_endpoint=SRC_COLLECTION, - destination_endpoint=DST_COLLECTION, + SRC_COLLECTION, + DST_COLLECTION, deadline=make_relative_deadline(datetime.timedelta(hours=1)), ) transfer_request.add_item(SRC_PATH, DST_PATH) diff --git a/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/submit_transfer_relative_deadline.py b/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/submit_transfer_relative_deadline.py index 96a5fbd15..22c4c7f68 100644 --- a/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/submit_transfer_relative_deadline.py +++ b/docs/user_guide/usage_patterns/data_transfer/transfer_relative_deadline/submit_transfer_relative_deadline.py @@ -32,8 +32,8 @@ def make_relative_deadline(offset: datetime.timedelta) -> str: transfer_client.add_app_data_access_scope(DST_COLLECTION) transfer_request = globus_sdk.TransferData( - source_endpoint=SRC_COLLECTION, - destination_endpoint=DST_COLLECTION, + SRC_COLLECTION, + DST_COLLECTION, deadline=make_relative_deadline(datetime.timedelta(hours=1)), ) transfer_request.add_item(SRC_PATH, DST_PATH) diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index 612b087fa..c4923d160 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -67,9 +67,7 @@ def add_app_transfer_data_access_scope( app = UserApp("myapp", client_id=NATIVE_APP_CLIENT_ID) client = TimersClient(app=app).add_app_transfer_data_access_scope(COLLECTION_ID) - transfer_data = TransferData( - source_endpoint=COLLECTION_ID, destination_endpoint=COLLECTION_ID - ) + transfer_data = TransferData(COLLECTION_ID, COLLECTION_ID) transfer_data.add_item("/staging/", "/active/") daily_timer = TransferTimer( @@ -155,15 +153,14 @@ def create_timer( .. code-block:: pycon - >>> transfer_client = TransferClient(...) - >>> transfer_data = TransferData(transfer_client, ...) - >>> timer_client = globus_sdk.TimersClient(...) + >>> transfer_data = TransferData(...) + >>> timers_client = globus_sdk.TimersClient(...) >>> create_doc = globus_sdk.TransferTimer( ... name="my-timer", ... schedule={"type": "recurring", "interval": 1800}, ... body=transfer_data, ... ) - >>> response = timer_client.create_timer(timer=create_doc) + >>> response = timers_client.create_timer(timer=create_doc) .. tab-item:: Example Response Data @@ -192,16 +189,15 @@ def create_job( **Examples** >>> from datetime import datetime, timedelta - >>> transfer_client = TransferClient(...) - >>> transfer_data = TransferData(transfer_client, ...) - >>> timer_client = globus_sdk.TimersClient(...) + >>> transfer_data = TransferData(...) + >>> timers_client = globus_sdk.TimersClient(...) >>> job = TimerJob.from_transfer_data( ... transfer_data, ... datetime.utcnow(), ... timedelta(days=14), ... name="my-timer-job" ... ) - >>> timer_result = timer_client.create_job(job) + >>> timer_result = timers_client.create_job(job) """ if isinstance(data, TransferTimer): raise exc.GlobusSDKUsageError( diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 4e51d7658..f46c3535b 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -1612,16 +1612,16 @@ def submit_transfer( .. code-block:: python - tc = globus_sdk.TransferClient(...) tdata = globus_sdk.TransferData( - tc, source_endpoint_id, destination_endpoint_id, label="SDK example", sync_level="checksum", ) - tdata.add_item("/source/path/dir/", "/dest/path/dir/", recursive=True) + tdata.add_item("/source/path/dir/", "/dest/path/dir/") tdata.add_item("/source/path/file.txt", "/dest/path/file.txt") + + tc = globus_sdk.TransferClient(...) transfer_result = tc.submit_transfer(tdata) print("task_id =", transfer_result["task_id"]) @@ -1636,7 +1636,7 @@ def submit_transfer( :ref: transfer/task_submit/#submit_transfer_task """ # noqa: E501 log.debug("TransferClient.submit_transfer(...)") - if "submission_id" not in data: + if "submission_id" not in data or data["submission_id"] is MISSING: log.debug("submit_transfer autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] return self.post("/v0.10/transfer", data=data) @@ -1661,10 +1661,11 @@ def submit_delete( .. code-block:: python - tc = globus_sdk.TransferClient(...) - ddata = globus_sdk.DeleteData(tc, endpoint_id, recursive=True) + ddata = globus_sdk.DeleteData(endpoint_id, recursive=True) ddata.add_item("/dir/to/delete/") ddata.add_item("/file/to/delete/file.txt") + + tc = globus_sdk.TransferClient(...) delete_result = tc.submit_delete(ddata) print("task_id =", delete_result["task_id"]) @@ -1679,7 +1680,7 @@ def submit_delete( :ref: transfer/task_submit/#submit_delete_task """ log.debug("TransferClient.submit_delete(...)") - if "submission_id" not in data: + if "submission_id" not in data or data["submission_id"] is MISSING: log.debug("submit_delete autofetching submission_id") data["submission_id"] = self.get_submission_id()["value"] return self.post("/v0.10/delete", data=data) diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index beaddde44..f1a28cd59 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -4,15 +4,11 @@ import logging import typing as t -from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload from globus_sdk._remarshal import stringify from globus_sdk._types import UUIDLike -if t.TYPE_CHECKING: - import globus_sdk - log = logging.getLogger(__name__) @@ -25,20 +21,12 @@ class DeleteData(GlobusPayload): At least one item must be added using :meth:`add_item `. - If ``submission_id`` isn't passed, one will be fetched automatically. The - submission ID can be pulled out of here to inspect, but the document - can be used as-is multiple times over to retry a potential submission - failure (so there shouldn't be any need to inspect it). - - :param transfer_client: A ``TransferClient`` instance which will be used to get a - submission ID if one is not supplied. Should be the same instance that is used - to submit the deletion. :param endpoint: The endpoint ID which is targeted by this deletion Task :param label: A string label for the Task - :param submission_id: A submission ID value fetched via - :meth:`get_submission_id `. - Defaults to using ``transfer_client.get_submission_id`` if a ``transfer_client`` - is provided + :param submission_id: A submission ID value fetched via :meth:`get_submission_id \ + `. By default, the SDK + will fetch and populate this field when :meth:`submit_delete \ + ` is called. :param recursive: Recursively delete subdirectories on the target endpoint [default: ``False``] :param ignore_missing: Ignore nonexistent files and directories instead of treating @@ -89,8 +77,7 @@ class DeleteData(GlobusPayload): def __init__( self, - transfer_client: globus_sdk.TransferClient | None = None, - endpoint: UUIDLike | MissingType = MISSING, + endpoint: UUIDLike, *, label: str | MissingType = MISSING, submission_id: UUIDLike | MissingType = MISSING, @@ -106,18 +93,11 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - # this must be checked explicitly to handle the fact that `transfer_client` is - # the first arg - if isinstance(endpoint, MissingType): - raise exc.GlobusSDKUsageError("endpoint is required") - self["DATA_TYPE"] = "delete" self["DATA"] = [] self["endpoint"] = endpoint self["label"] = label - self["submission_id"] = submission_id or ( - transfer_client.get_submission_id()["value"] if transfer_client else MISSING - ) + self["submission_id"] = submission_id self["deadline"] = stringify(deadline) self["local_user"] = local_user self["recursive"] = recursive diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index a654ff088..bf9b52289 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -4,14 +4,10 @@ import logging import typing as t -from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload from globus_sdk._types import UUIDLike -if t.TYPE_CHECKING: - import globus_sdk - log = logging.getLogger(__name__) _sync_level_dict: dict[t.Literal["exists", "size", "mtime", "checksum"], int] = { "exists": 0, @@ -46,20 +42,13 @@ class TransferData(GlobusPayload): At least one item must be added using :meth:`add_item `. - If ``submission_id`` isn't passed, one will be fetched automatically. The - submission ID can be pulled out of here to inspect, but the document - can be used as-is multiple times over to retry a potential submission - failure (so there shouldn't be any need to inspect it). - - :param transfer_client: A ``TransferClient`` instance which will be used to get a - submission ID if one is not supplied. Should be the same instance that is used - to submit the transfer. :param source_endpoint: The endpoint ID of the source endpoint :param destination_endpoint: The endpoint ID of the destination endpoint :param label: A string label for the Task :param submission_id: A submission ID value fetched via :meth:`get_submission_id \ - `. Defaults to using - ``transfer_client.get_submission_id`` + `. By default, the SDK + will fetch and populate this field when :meth:`submit_transfer \ + ` is called. :param sync_level: The method used to compare items between the source and destination. One of ``"exists"``, ``"size"``, ``"mtime"``, or ``"checksum"`` See the section below on sync-level for an explanation of values. @@ -165,9 +154,8 @@ class TransferData(GlobusPayload): def __init__( self, - transfer_client: globus_sdk.TransferClient | None = None, - source_endpoint: UUIDLike | MissingType = MISSING, - destination_endpoint: UUIDLike | MissingType = MISSING, + source_endpoint: UUIDLike, + destination_endpoint: UUIDLike, *, label: str | MissingType = MISSING, submission_id: UUIDLike | MissingType = MISSING, @@ -191,22 +179,13 @@ def __init__( additional_fields: dict[str, t.Any] | None = None, ) -> None: super().__init__() - # these must be checked explicitly to handle the fact that `transfer_client` is - # the first arg - if isinstance(source_endpoint, MissingType): - raise exc.GlobusSDKUsageError("source_endpoint is required") - if isinstance(destination_endpoint, MissingType): - raise exc.GlobusSDKUsageError("destination_endpoint is required") - log.debug("Creating a new TransferData object") self["DATA_TYPE"] = "transfer" self["DATA"] = [] self["source_endpoint"] = source_endpoint self["destination_endpoint"] = destination_endpoint self["label"] = label - self["submission_id"] = submission_id or ( - transfer_client.get_submission_id()["value"] if transfer_client else MISSING - ) + self["submission_id"] = submission_id self["recursive_symlinks"] = recursive_symlinks self["deadline"] = deadline self["source_local_user"] = source_local_user diff --git a/tests/functional/services/timers/test_jobs.py b/tests/functional/services/timers/test_jobs.py index d988c2ef9..54160d7e5 100644 --- a/tests/functional/services/timers/test_jobs.py +++ b/tests/functional/services/timers/test_jobs.py @@ -39,9 +39,7 @@ def test_get_job_errors(client): ) def test_create_job(client, start, interval): meta = load_response(client.create_job).metadata - transfer_data = TransferData( - source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID - ) + transfer_data = TransferData(GO_EP1_ID, GO_EP2_ID) with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): timer_job = TimerJob.from_transfer_data(transfer_data, start, interval) response = client.create_job(timer_job) @@ -68,9 +66,7 @@ def test_create_job(client, start, interval): def test_create_job_validation_error(client): meta = load_response(client.create_job, case="validation_error").metadata - transfer_data = TransferData( - source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID - ) + transfer_data = TransferData(GO_EP1_ID, GO_EP2_ID) with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): timer_job = TimerJob.from_transfer_data( transfer_data, "2022-04-05T06:00:00", 1800 diff --git a/tests/functional/services/transfer/test_task_submit.py b/tests/functional/services/transfer/test_task_submit.py index 75fafb3e6..822ad0b5b 100644 --- a/tests/functional/services/transfer/test_task_submit.py +++ b/tests/functional/services/transfer/test_task_submit.py @@ -16,9 +16,7 @@ def test_transfer_submit_failure(client): meta = load_response(client.submit_transfer, case="failure").metadata with pytest.raises(TransferAPIError) as excinfo: - client.submit_transfer( - TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) - ) + client.submit_transfer(TransferData(GO_EP1_ID, GO_EP2_ID)) assert excinfo.value.http_status == 400 assert excinfo.value.request_id == meta["request_id"] @@ -30,8 +28,8 @@ def test_transfer_submit_success(client): meta = load_response(client.submit_transfer).metadata tdata = TransferData( - source_endpoint=GO_EP1_ID, - destination_endpoint=GO_EP2_ID, + GO_EP1_ID, + GO_EP2_ID, label="mytask", sync_level="exists", deadline="2018-06-01", diff --git a/tests/non-pytest/mypy-ignore-tests/transfer_data.py b/tests/non-pytest/mypy-ignore-tests/transfer_data.py index 963a1393a..fa6e1d1e9 100644 --- a/tests/non-pytest/mypy-ignore-tests/transfer_data.py +++ b/tests/non-pytest/mypy-ignore-tests/transfer_data.py @@ -1,21 +1,20 @@ import uuid -from globus_sdk import TransferClient, TransferData +from globus_sdk import TransferData # simple usage, ok -tc = TransferClient() -TransferData(tc, "srcep", "destep") +TransferData("srcep", "destep") # can set sync level -TransferData(tc, "srcep", "destep", sync_level=1) -TransferData(tc, "srcep", "destep", sync_level="exists") +TransferData("srcep", "destep", sync_level=1) +TransferData("srcep", "destep", sync_level="exists") # unknown int values are allowed -TransferData(tc, "srcep", "destep", sync_level=100) +TransferData("srcep", "destep", sync_level=100) # unknown str values are rejected (Literal) -TransferData(tc, "srcep", "destep", sync_level="sizes") # type: ignore[arg-type] +TransferData("srcep", "destep", sync_level="sizes") # type: ignore[arg-type] # TransferData.add_filter_rule -tdata = TransferData(tc, uuid.UUID(), uuid.UUID()) +tdata = TransferData(uuid.UUID(), uuid.UUID()) tdata.add_filter_rule("*.tgz") tdata.add_filter_rule("*.tgz", method="exclude") tdata.add_filter_rule("*.tgz", type="file") diff --git a/tests/unit/helpers/test_timer.py b/tests/unit/helpers/test_timer.py index ba8a51202..c66a0498f 100644 --- a/tests/unit/helpers/test_timer.py +++ b/tests/unit/helpers/test_timer.py @@ -15,7 +15,7 @@ def test_timer_from_transfer_data_ok(): - tdata = TransferData(None, GO_EP1_ID, GO_EP2_ID) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID) with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): job = TimerJob.from_transfer_data(tdata, "2022-01-01T00:00:00Z", 600) assert "callback_body" in job @@ -30,14 +30,14 @@ def test_timer_from_transfer_data_ok(): "badkey, value", (("submission_id", "foo"), ("skip_activation_check", True)) ) def test_timer_from_transfer_data_rejects_forbidden_keys(badkey, value): - tdata = TransferData(None, GO_EP1_ID, GO_EP2_ID, **{badkey: value}) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID, **{badkey: value}) with pytest.raises(ValueError): with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): TimerJob.from_transfer_data(tdata, "2022-01-01T00:00:00Z", 600) def test_transfer_timer_ok(): - tdata = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID) timer = TransferTimer(body=tdata, name="foo timer", schedule={"type": "once"}) assert timer["name"] == "foo timer" assert timer["schedule"]["type"] == "once" diff --git a/tests/unit/helpers/test_transfer.py b/tests/unit/helpers/test_transfer.py index 75dc857c2..3d105a880 100644 --- a/tests/unit/helpers/test_transfer.py +++ b/tests/unit/helpers/test_transfer.py @@ -1,42 +1,30 @@ import pytest -from globus_sdk import ( - MISSING, - DeleteData, - GlobusSDKUsageError, - TransferClient, - TransferData, -) -from globus_sdk._testing import load_response +from globus_sdk import MISSING, DeleteData, TransferData from globus_sdk.services.transfer.client import _format_filter from tests.common import GO_EP1_ID, GO_EP2_ID -def test_transfer_init_simple(): +def test_transfer_init_no_params(): """ - Creates TransferData objects with and without parameters, - Verifies TransferData field initialization + Creates a TransferData object without optional parameters and + verifies field initialization. """ - tc = TransferClient() - meta = load_response(tc.get_submission_id).metadata # default init - tdata = TransferData(tc, GO_EP1_ID, GO_EP2_ID) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID) assert tdata["DATA_TYPE"] == "transfer" assert tdata["source_endpoint"] == GO_EP1_ID assert tdata["destination_endpoint"] == GO_EP2_ID - assert tdata["submission_id"] == meta["submission_id"] + assert tdata["submission_id"] is MISSING assert "DATA" in tdata assert len(tdata["DATA"]) == 0 def test_transfer_init_w_params(): - tc = TransferClient() - meta = load_response(tc.get_submission_id).metadata # init with params label = "label" params = {"param1": "value1", "param2": "value2"} tdata = TransferData( - tc, GO_EP1_ID, GO_EP2_ID, label=label, @@ -44,48 +32,18 @@ def test_transfer_init_w_params(): additional_fields=params, ) assert tdata["label"] == label - assert tdata["submission_id"] == meta["submission_id"] + assert tdata["submission_id"] is MISSING # sync_level of "exists" should be converted to 0 assert tdata["sync_level"] == 0 for par in params: assert tdata[par] == params[par] -def test_transfer_init_no_client(): - tdata1 = TransferData(None, GO_EP1_ID, GO_EP2_ID) - tdata2 = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) - tdata3 = TransferData( - source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID, transfer_client=None - ) - for tdata in (tdata1, tdata2, tdata3): - assert tdata["DATA_TYPE"] == "transfer" - assert tdata["source_endpoint"] == GO_EP1_ID - assert tdata["destination_endpoint"] == GO_EP2_ID - assert tdata["submission_id"] is MISSING - assert "DATA" in tdata - assert len(tdata["DATA"]) == 0 - - -@pytest.mark.parametrize( - "tdata_args", - [ - (), - (GO_EP1_ID, GO_EP2_ID), - (MISSING, MISSING, MISSING), - (MISSING, GO_EP1_ID, MISSING), - (MISSING, MISSING, GO_EP2_ID), - ], -) -def test_transfer_init_rejects_bad_usage(tdata_args): - with pytest.raises(GlobusSDKUsageError): - TransferData(*tdata_args) - - def test_transfer_add_item(): """ Adds items to TransferData, verifies results """ - tdata = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID) # add item source_path = "source/path/" dest_path = "dest/path/" @@ -143,7 +101,7 @@ def test_transfer_add_symlink_item(): """ Adds a transfer_symlink_item to TransferData, verifies results """ - tdata = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID) # add item source_path = "source/path/" dest_path = "dest/path/" @@ -156,16 +114,20 @@ def test_transfer_add_symlink_item(): assert data["destination_path"] == dest_path -def test_delete_init_with_client(): - """ - Verifies DeleteData field initialization - """ - tc = TransferClient() - load_response(tc.get_submission_id) - ddata = DeleteData(tc, GO_EP1_ID) +@pytest.mark.parametrize( + "args, kwargs", + ( + ((GO_EP1_ID,), {}), + ((), {"endpoint": GO_EP1_ID}), + ), +) +def test_delete_data_noparams_init(args, kwargs): + # the minimal, required argument is the endpoint ID -- less than that results + # in a TypeError because the signature is not obeyed + ddata = DeleteData(*args, **kwargs) assert ddata["DATA_TYPE"] == "delete" assert ddata["endpoint"] == GO_EP1_ID - assert "submission_id" in ddata + assert ddata["submission_id"] is MISSING assert "DATA" in ddata assert len(ddata["DATA"]) == 0 @@ -180,48 +142,23 @@ def test_delete_init_with_client(): ), ) def test_delete_init_with_supported_parameters(add_kwargs): - ddata = DeleteData(endpoint=GO_EP1_ID, **add_kwargs) + ddata = DeleteData(GO_EP1_ID, **add_kwargs) for k, v in add_kwargs.items(): assert ddata[k] == v def test_delete_init_with_additional_fields(): params = {"param1": "value1", "param2": "value2"} - ddata = DeleteData(endpoint=GO_EP1_ID, additional_fields=params) + ddata = DeleteData(GO_EP1_ID, additional_fields=params) assert ddata["param1"] == "value1" assert ddata["param2"] == "value2" -@pytest.mark.parametrize( - "args, kwargs", - ( - ((None, GO_EP1_ID), {}), - ((), {"endpoint": GO_EP1_ID}), - ((), {"endpoint": GO_EP1_ID, "transfer_client": None}), - ), -) -def test_delete_init_no_client(args, kwargs): - ddata = DeleteData(*args, **kwargs) - assert ddata["DATA_TYPE"] == "delete" - assert ddata["endpoint"] == GO_EP1_ID - assert ddata["submission_id"] is MISSING - assert "DATA" in ddata - assert len(ddata["DATA"]) == 0 - - -@pytest.mark.parametrize( - "ddata_args", [(), (GO_EP1_ID,), (MISSING, MISSING), (GO_EP1_ID, MISSING)] -) -def test_delete_init_rejects_bad_usage(ddata_args): - with pytest.raises(GlobusSDKUsageError): - DeleteData(*ddata_args) - - def test_delete_add_item(): """ Adds items to DeleteData, verifies results """ - ddata = DeleteData(endpoint=GO_EP1_ID) + ddata = DeleteData(GO_EP1_ID) # add normal item path = "source/path/" @@ -242,7 +179,7 @@ def test_delete_add_item(): def test_delete_iter_items(): - ddata = DeleteData(endpoint=GO_EP1_ID) + ddata = DeleteData(GO_EP1_ID) # add item ddata.add_item("abc/") ddata.add_item("def/") @@ -261,7 +198,7 @@ def check_item(x, path): def test_transfer_iter_items(): - tdata = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID) tdata.add_item("source/abc.txt", "dest/abc.txt") tdata.add_item("source/def/", "dest/def/", recursive=True) @@ -294,10 +231,8 @@ def test_notification_options(n_succeeded, n_failed, n_inactive): if n_inactive is not None: notify_kwargs["notify_on_inactive"] = n_inactive - ddata = DeleteData(endpoint=GO_EP1_ID, **notify_kwargs) - tdata = TransferData( - source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID, **notify_kwargs - ) + ddata = DeleteData(GO_EP1_ID, **notify_kwargs) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID, **notify_kwargs) def _default(x): return x if x is not None else True @@ -335,17 +270,9 @@ def _default(x): def test_transfer_sync_levels_result(sync_level, result): if isinstance(result, type) and issubclass(result, Exception): with pytest.raises(result): - TransferData( - source_endpoint=GO_EP1_ID, - destination_endpoint=GO_EP2_ID, - sync_level=sync_level, - ) + TransferData(GO_EP1_ID, GO_EP2_ID, sync_level=sync_level) else: - tdata = TransferData( - source_endpoint=GO_EP1_ID, - destination_endpoint=GO_EP2_ID, - sync_level=sync_level, - ) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID, sync_level=sync_level) assert tdata["sync_level"] == result @@ -354,11 +281,9 @@ def test_transfer_sync_levels_result(sync_level, result): def test_skip_activation_check_supported(datatype, value): def create(**kwargs): if datatype == "transfer": - return TransferData( - source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID, **kwargs - ) + return TransferData(GO_EP1_ID, GO_EP2_ID, **kwargs) else: - return DeleteData(endpoint=GO_EP1_ID, **kwargs) + return DeleteData(GO_EP1_ID, **kwargs) if value is None: # not present if not provided as a param or provided as explicit None @@ -374,7 +299,7 @@ def create(**kwargs): def test_add_filter_rule(): - tdata = TransferData(source_endpoint=GO_EP1_ID, destination_endpoint=GO_EP2_ID) + tdata = TransferData(GO_EP1_ID, GO_EP2_ID) assert "filter_rules" not in tdata tdata.add_filter_rule("*.tgz", type="file") From 224399698331e6e8adadad196fd744d3f987722d Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Fri, 6 Jun 2025 17:36:42 -0500 Subject: [PATCH 050/176] Begin converting Auth clients to use MISSING Co-authored-by: Stephen Rosen <1300022+sirosen@users.noreply.github.com> --- src/globus_sdk/_missing.py | 15 ++++ .../login_flows/login_flow_manager.py | 24 ++++-- src/globus_sdk/services/auth/_common.py | 9 +- .../services/auth/client/base_login_client.py | 68 +++++++-------- .../auth/client/confidential_client.py | 83 +++++++++---------- .../services/auth/client/native_client.py | 7 +- .../auth/flow_managers/authorization_code.py | 6 +- .../services/auth/flow_managers/native_app.py | 32 +++---- .../services/auth/test_auth_client_flow.py | 21 ++--- tests/unit/helpers/test_auth_flow_managers.py | 8 +- 10 files changed, 144 insertions(+), 129 deletions(-) diff --git a/src/globus_sdk/_missing.py b/src/globus_sdk/_missing.py index c1fe7b1de..94c96e629 100644 --- a/src/globus_sdk/_missing.py +++ b/src/globus_sdk/_missing.py @@ -8,6 +8,8 @@ import typing as t +T = t.TypeVar("T") + class MissingType: def __init__(self) -> None: @@ -56,3 +58,16 @@ def filter_missing(data: dict[str, t.Any] | None) -> dict[str, t.Any] | None: if data is None: return None return {k: v for k, v in data.items() if v is not MISSING} + + +def none2missing(obj: T | None) -> T | MissingType: + """ + A converter for interfaces which take "nullable" to mean "omittable", to + adapt them to usage sites which require use of MISSING for omittable + elements. + + :param obj: The nullable object to convert to an omittable object. + """ + if obj is None: + return MISSING + return obj diff --git a/src/globus_sdk/login_flows/login_flow_manager.py b/src/globus_sdk/login_flows/login_flow_manager.py index e37c358ef..7847215e0 100644 --- a/src/globus_sdk/login_flows/login_flow_manager.py +++ b/src/globus_sdk/login_flows/login_flow_manager.py @@ -3,6 +3,7 @@ import abc import globus_sdk +from globus_sdk._missing import none2missing from globus_sdk.gare import GlobusAuthorizationParameters @@ -51,14 +52,21 @@ def _get_authorize_url( """ self._oauth2_start_flow(auth_parameters, redirect_uri) - session_required_single_domain = auth_parameters.session_required_single_domain + session_required_single_domain = none2missing( + auth_parameters.session_required_single_domain + ) + prompt = none2missing(auth_parameters.prompt) return self.login_client.oauth2_get_authorize_url( - session_required_identities=auth_parameters.session_required_identities, - session_required_single_domain=session_required_single_domain, - session_required_policies=auth_parameters.session_required_policies, - session_required_mfa=auth_parameters.session_required_mfa, - session_message=auth_parameters.session_message, - prompt=auth_parameters.prompt, # type: ignore + session_required_identities=none2missing( + auth_parameters.session_required_identities + ), + session_required_single_domain=none2missing(session_required_single_domain), + session_required_policies=none2missing( + auth_parameters.session_required_policies + ), + session_required_mfa=none2missing(auth_parameters.session_required_mfa), + session_message=none2missing(auth_parameters.session_message), + prompt=prompt, # type: ignore[arg-type] ) def _oauth2_start_flow( @@ -83,7 +91,7 @@ def _oauth2_start_flow( requested_scopes, redirect_uri=redirect_uri, refresh_tokens=self.request_refresh_tokens, - prefill_named_grant=self.native_prefill_named_grant, + prefill_named_grant=none2missing(self.native_prefill_named_grant), ) elif isinstance(login_client, globus_sdk.ConfidentialAppAuthClient): login_client.oauth2_start_flow( diff --git a/src/globus_sdk/services/auth/_common.py b/src/globus_sdk/services/auth/_common.py index 2f417c468..9cb5f2c96 100644 --- a/src/globus_sdk/services/auth/_common.py +++ b/src/globus_sdk/services/auth/_common.py @@ -7,6 +7,7 @@ import jwt 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 @@ -77,7 +78,7 @@ def get_openid_configuration(self) -> GlobusHTTPResponse: ... @t.overload def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any], + openid_configuration: GlobusHTTPResponse | dict[str, t.Any] | MissingType, *, as_pem: t.Literal[True], ) -> RSAPublicKey: ... @@ -85,14 +86,16 @@ def get_jwk( @t.overload def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any], + openid_configuration: GlobusHTTPResponse | dict[str, t.Any] | MissingType, *, as_pem: t.Literal[False], ) -> dict[str, t.Any]: ... def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any] = None, + openid_configuration: ( + GlobusHTTPResponse | dict[str, t.Any] | MissingType + ) = MISSING, *, as_pem: bool = False, ) -> RSAPublicKey | dict[str, t.Any]: ... diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index ab9bb9116..194cfa55a 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -6,6 +6,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from globus_sdk import _guards, client, exc +from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer, NullAuthorizer @@ -96,7 +97,7 @@ def get_openid_configuration(self) -> GlobusHTTPResponse: @t.overload def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any], + openid_configuration: GlobusHTTPResponse | dict[str, t.Any] | MissingType, *, as_pem: t.Literal[True], ) -> RSAPublicKey: ... @@ -104,7 +105,7 @@ def get_jwk( @t.overload def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any], + openid_configuration: GlobusHTTPResponse | dict[str, t.Any] | MissingType, *, as_pem: t.Literal[False], ) -> dict[str, t.Any]: ... @@ -118,7 +119,9 @@ def get_jwk( # an AuthClient which it uses def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any] = None, + openid_configuration: ( + GlobusHTTPResponse | dict[str, t.Any] | MissingType + ) = MISSING, *, as_pem: bool = False, ) -> RSAPublicKey | dict[str, t.Any]: @@ -131,7 +134,7 @@ def get_jwk( When not provided, it will be fetched automatically. :param as_pem: Decode the JWK to an RSA PEM key, typically for JWT decoding """ - if openid_configuration is None: + if isinstance(openid_configuration, MissingType): log.debug("No OIDC Config provided, autofetching...") openid_configuration = self.get_openid_configuration() jwk_data = get_jwk_data( @@ -142,12 +145,16 @@ def get_jwk( def oauth2_get_authorize_url( self, *, - session_required_identities: UUIDLike | t.Iterable[UUIDLike] | None = None, - session_required_single_domain: str | t.Iterable[str] | None = None, - session_required_policies: UUIDLike | t.Iterable[UUIDLike] | None = None, - session_required_mfa: bool | None = None, - session_message: str | None = None, - prompt: t.Literal["login"] | None = None, + session_required_identities: ( + UUIDLike | t.Iterable[UUIDLike] | MissingType + ) = MISSING, + session_required_single_domain: str | t.Iterable[str] | MissingType = MISSING, + session_required_policies: ( + UUIDLike | t.Iterable[UUIDLike] | MissingType + ) = MISSING, + session_required_mfa: bool | MissingType = MISSING, + session_message: str | MissingType = MISSING, + prompt: t.Literal["login"] | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> str: """ @@ -180,26 +187,15 @@ def oauth2_get_authorize_url( "Call the oauth2_start_flow() method on this " "AuthClient to resolve" ) - if query_params is None: - query_params = {} - if session_required_identities is not None: - query_params["session_required_identities"] = commajoin( - session_required_identities - ) - if session_required_single_domain is not None: - query_params["session_required_single_domain"] = commajoin( - session_required_single_domain - ) - if session_required_policies is not None: - query_params["session_required_policies"] = commajoin( - session_required_policies - ) - if session_required_mfa is not None: - query_params["session_required_mfa"] = session_required_mfa - if session_message is not None: - query_params["session_message"] = session_message - if prompt is not None: - query_params["prompt"] = prompt + query_params = { + "session_required_identities": commajoin(session_required_identities), + "session_required_single_domain": commajoin(session_required_single_domain), + "session_required_policies": commajoin(session_required_policies), + "session_required_mfa": session_required_mfa, + "session_message": session_message, + "prompt": prompt, + **(query_params or {}), + } auth_url = self.current_oauth2_flow_manager.get_authorize_url( query_params=query_params ) @@ -292,9 +288,7 @@ def oauth2_validate_token( if no_authentication and self.client_id: log.debug("Validating token with unauthenticated client") body.update({"client_id": self.client_id}) - - if body_params: - body.update(body_params) + body.update(body_params or {}) return self.post("/v2/oauth2/token/validate", data=body, encoding="form") def oauth2_revoke_token( @@ -337,9 +331,7 @@ def oauth2_revoke_token( if no_authentication and self.client_id: log.debug("Revoking token with unauthenticated client") body.update({"client_id": self.client_id}) - - if body_params: - body.update(body_params) + body.update(body_params or {}) return self.post("/v2/oauth2/token/revoke", data=body, encoding="form") @t.overload @@ -403,9 +395,7 @@ def oauth2_token( log.debug("Fetching new token from Globus Auth") # use the fact that requests implicitly encodes the `data` parameter as # a form POST - data = dict(form_data) - if body_params: - data.update(body_params) + data = {**dict(form_data), **(body_params or {})} return response_class( self.post( "/v2/oauth2/token", diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index ce5072fda..97d2e0717 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -65,8 +65,8 @@ def __init__( def get_identities( self, *, - usernames: t.Iterable[str] | str | None = None, - ids: t.Iterable[UUIDLike] | UUIDLike | None = None, + usernames: t.Iterable[str] | str | MissingType = MISSING, + ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, provision: bool = False, query_params: dict[str, t.Any] | None = None, ) -> GetIdentitiesResponse: @@ -92,18 +92,12 @@ def get_identities( "Get a token via `oauth2_client_credentials_tokens` " "and use that to call the API instead." ) - - if query_params is None: - query_params = {} - - if usernames is not None: - query_params["usernames"] = commajoin(usernames) - query_params["provision"] = ( - "false" if str(provision).lower() == "false" else "true" - ) - if ids is not None: - query_params["ids"] = commajoin(ids) - + query_params = { + "usernames": commajoin(usernames), + "provision": str(provision).lower(), + "ids": commajoin(ids), + **(query_params or {}), + } return GetIdentitiesResponse( self.get("/v2/api/identities", query_params=query_params) ) @@ -264,24 +258,24 @@ def oauth2_get_dependent_tokens( form_data = { "grant_type": "urn:globus:auth:grant_type:dependent_token", "token": token, + # the internal parameter is 'access_type', but using the name + # 'refresh_tokens' is consistent with the rest of the SDK and better + # communicates expectations back to the user than the OAuth2 spec wording + "access_type": "offline" if refresh_tokens else MISSING, + "scope": ( + " ".join(strseq_iter(scope)) + if not isinstance(scope, MissingType) + else scope + ), + **(additional_params or {}), } - # the internal parameter is 'access_type', but using the name 'refresh_tokens' - # is consistent with the rest of the SDK and better communicates expectations - # back to the user than the OAuth2 spec wording - if refresh_tokens: - form_data["access_type"] = "offline" - if not isinstance(scope, MissingType): - form_data["scope"] = " ".join(strseq_iter(scope)) - if additional_params: - form_data.update(additional_params) - return self.oauth2_token(form_data, response_class=OAuthDependentTokenResponse) def oauth2_token_introspect( self, token: str, *, - include: str | None = None, + include: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ @@ -320,9 +314,10 @@ def oauth2_token_introspect( :ref: auth/reference/#token_introspection_post_v2_oauth2_token_introspect """ # noqa: E501 log.debug("Checking token validity (introspect)") - body = {"token": token} - if include is not None: - body["include"] = include + body = { + "token": token, + "include": include, + } return self.post( "/v2/oauth2/token/introspect", data=body, @@ -352,7 +347,7 @@ def create_child_client( privacy_policy: str | MissingType = MISSING, required_idp: UUIDLike | MissingType = MISSING, preselect_idp: UUIDLike | MissingType = MISSING, - additional_fields: dict[str, t.Any] | MissingType = MISSING, + additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ Create a new client. Requires the ``manage_projects`` scope. @@ -447,6 +442,17 @@ def create_child_client( "AuthClient.create_client requires either 'public_client' or " "'client_type'." ) + # terms_and_conditions and privacy_policy must both be set or unset + if bool(terms_and_conditions) ^ bool(privacy_policy): + raise exc.GlobusSDKUsageError( + "terms_and_conditions and privacy_policy must both be set or unset" + ) + links: dict[str, str | MissingType] | MissingType = MISSING + if terms_and_conditions or privacy_policy: + links = { + "terms_and_conditions": terms_and_conditions, + "privacy_policy": privacy_policy, + } body: dict[str, t.Any] = { "name": name, @@ -455,23 +461,12 @@ def create_child_client( "preselect_idp": preselect_idp, "public_client": public_client, "client_type": client_type, + "redirect_uris": strseq_listify(redirect_uris), + "links": links, + **(additional_fields or {}), } - if not isinstance(redirect_uris, MissingType): - body["redirect_uris"] = strseq_listify(redirect_uris) - - # terms_and_conditions and privacy_policy must both be set or unset - if bool(terms_and_conditions) ^ bool(privacy_policy): - raise exc.GlobusSDKUsageError( - "terms_and_conditions and privacy_policy must both be set or unset" - ) - links: dict[str, str | MissingType] = { - "terms_and_conditions": terms_and_conditions, - "privacy_policy": privacy_policy, - } - if terms_and_conditions or privacy_policy: - body["links"] = links - if not isinstance(additional_fields, MissingType): + if additional_fields is not None: body.update(additional_fields) return self.post("/v2/api/clients", data={"client": body}) diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index b63eaff2b..5c8667c7f 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -3,6 +3,7 @@ import logging import typing as t +from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import ScopeCollectionType, UUIDLike from globus_sdk.authorizers import NullAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -52,11 +53,11 @@ def oauth2_start_flow( self, requested_scopes: ScopeCollectionType, *, - redirect_uri: str | None = None, + redirect_uri: str | MissingType = MISSING, state: str = "_default", - verifier: str | None = None, + verifier: str | MissingType = MISSING, refresh_tokens: bool = False, - prefill_named_grant: str | None = None, + prefill_named_grant: str | MissingType = MISSING, ) -> GlobusNativeAppFlowManager: """ Starts a Native App OAuth2 flow. diff --git a/src/globus_sdk/services/auth/flow_managers/authorization_code.py b/src/globus_sdk/services/auth/flow_managers/authorization_code.py index 289028af2..3373425fa 100644 --- a/src/globus_sdk/services/auth/flow_managers/authorization_code.py +++ b/src/globus_sdk/services/auth/flow_managers/authorization_code.py @@ -4,6 +4,7 @@ import typing as t import urllib.parse +from globus_sdk._missing import filter_missing from globus_sdk._types import ScopeCollectionType from globus_sdk._utils import slash_join @@ -99,10 +100,9 @@ def get_authorize_url(self, query_params: dict[str, t.Any] | None = None) -> str "state": self.state, "response_type": "code", "access_type": (self.refresh_tokens and "offline") or "online", + **(query_params or {}), } - if query_params: - params.update(query_params) - + params = filter_missing(params) encoded_params = urllib.parse.urlencode(params) return f"{authorize_base_url}?{encoded_params}" diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index 06766a663..963b55f31 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -8,6 +8,7 @@ import typing as t 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 @@ -22,8 +23,8 @@ log = logging.getLogger(__name__) -def make_native_app_challenge( - verifier: str | None = None, +def _make_native_app_challenge( + verifier: str | MissingType = MISSING, ) -> tuple[str, str]: """ Produce a challenge and verifier for the Native App flow. @@ -41,22 +42,24 @@ def make_native_app_challenge( contain the following characters: [a-zA-Z0-9~_.-]. """ - if verifier: + if isinstance(verifier, str): if not 43 <= len(verifier) <= 128: raise GlobusSDKUsageError( f"verifier must be 43-128 characters long: {len(verifier)}" ) if bool(re.search(r"[^a-zA-Z0-9~_.-]", verifier)): raise GlobusSDKUsageError("verifier contained invalid characters") + + code_verifier: str = verifier else: log.debug( "Autogenerating verifier secret. On low-entropy systems " "this may be insecure" ) + code_verifier = ( + base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").rstrip("=") + ) - code_verifier = verifier or base64.urlsafe_b64encode(os.urandom(32)).decode( - "utf-8" - ).rstrip("=") # hash it, pull out a digest hashed_verifier = hashlib.sha256(code_verifier.encode("utf-8")).digest() # urlsafe base64 encode that hash and strip the padding @@ -101,11 +104,11 @@ def __init__( self, auth_client: globus_sdk.NativeAppAuthClient, requested_scopes: ScopeCollectionType, - redirect_uri: str | None = None, + redirect_uri: str | MissingType = MISSING, state: str = "_default", - verifier: str | None = None, + verifier: str | MissingType = MISSING, refresh_tokens: bool = False, - prefill_named_grant: str | None = None, + prefill_named_grant: str | MissingType = MISSING, ) -> None: self.auth_client = auth_client @@ -133,7 +136,7 @@ def __init__( # make a challenge and secret to keep # if the verifier is provided, it will just be passed back to us, and # if not, one will be generated - self.verifier, self.challenge = make_native_app_challenge(verifier) + self.verifier, self.challenge = _make_native_app_challenge(verifier) # store the remaining parameters directly, with no transformation self.refresh_tokens = refresh_tokens @@ -150,7 +153,7 @@ def __init__( f"verifier=,challenge={self.challenge}" ) - if prefill_named_grant is not None: + if not isinstance(prefill_named_grant, MissingType): log.debug(f"prefill_named_grant={self.prefill_named_grant}") def get_authorize_url(self, query_params: dict[str, t.Any] | None = None) -> str: @@ -181,11 +184,10 @@ def get_authorize_url(self, query_params: dict[str, t.Any] | None = None) -> str "code_challenge": self.challenge, "code_challenge_method": "S256", "access_type": (self.refresh_tokens and "offline") or "online", + "prefill_named_grant": self.prefill_named_grant, } - if self.prefill_named_grant is not None: - params["prefill_named_grant"] = self.prefill_named_grant - if query_params: - params.update(query_params) + params.update(query_params or {}) + params = filter_missing(params) encoded_params = urllib.parse.urlencode(params) return f"{authorize_base_url}?{encoded_params}" diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index 5e7943140..87cac2678 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -4,9 +4,10 @@ import pytest import globus_sdk +from globus_sdk._missing import MISSING, MissingType from globus_sdk._testing import load_response from globus_sdk.scopes import TransferScopes -from globus_sdk.services.auth.flow_managers.native_app import make_native_app_challenge +from globus_sdk.services.auth.flow_managers.native_app import _make_native_app_challenge CLIENT_ID = "d0f1d9b0-bd81-4108-be74-ea981664453a" @@ -62,7 +63,7 @@ class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): prompt_options = ("login",) # Seed an all-`None` option test, then use a loop to fill in the rest. # The number of parameters here must match the test parameters: -_ALL_SESSION_PARAM_COMBINATIONS = [(None,) * 6] +_ALL_SESSION_PARAM_COMBINATIONS = [(MISSING,) * 6] for idx, options in enumerate( ( domain_options, @@ -74,7 +75,7 @@ class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): ) ): for option in options: - parameters = [None] * 6 + parameters = [MISSING] * 6 parameters[idx] = option _ALL_SESSION_PARAM_COMBINATIONS.append(tuple(parameters)) @@ -130,7 +131,7 @@ def test_oauth2_get_authorize_url_supports_session_params( "session_required_single_domain" if domain_option else None, "session_required_identities" if identity_option else None, "session_required_policies" if policy_option else None, - "session_required_mfa" if mfa_option is not None else None, + "session_required_mfa" if not isinstance(mfa_option, MissingType) else None, "prompt" if prompt_option else None, } expected_params_keys.discard(None) @@ -147,7 +148,7 @@ def test_oauth2_get_authorize_url_supports_session_params( assert expected_params_keys <= parsed_params_keys assert (unexpected_query_params - parsed_params_keys) == unexpected_query_params - if domain_option is not None: + if domain_option is not MISSING: strized_option = ( ",".join(str(x) for x in domain_option) if isinstance(domain_option, list) @@ -155,7 +156,7 @@ def test_oauth2_get_authorize_url_supports_session_params( ) assert parsed_params["session_required_single_domain"] == [strized_option] - if identity_option is not None: + if identity_option is not MISSING: strized_option = ( ",".join(str(x) for x in identity_option) if isinstance(identity_option, list) @@ -163,7 +164,7 @@ def test_oauth2_get_authorize_url_supports_session_params( ) assert parsed_params["session_required_identities"] == [strized_option] - if policy_option is not None: + if policy_option is not MISSING: strized_option = ( ",".join(str(x) for x in policy_option) if isinstance(policy_option, list) @@ -171,11 +172,11 @@ def test_oauth2_get_authorize_url_supports_session_params( ) assert parsed_params["session_required_policies"] == [strized_option] - if mfa_option is not None: + if mfa_option is not MISSING: strized_option = "True" if mfa_option else "False" assert parsed_params["session_required_mfa"] == [strized_option] - if prompt_option is not None: + if prompt_option is not MISSING: assert parsed_params["prompt"] == [prompt_option] @@ -218,7 +219,7 @@ def test_oauth2_get_authorize_url_native_custom_params(native_client): # get url_and validate results url_res = native_client.oauth2_get_authorize_url() - verifier, remade_challenge = make_native_app_challenge("a" * 43) + verifier, remade_challenge = _make_native_app_challenge("a" * 43) parsed_url = urllib.parse.urlparse(url_res) assert f"https://{parsed_url.netloc}/" == native_client.base_url assert parsed_url.path == "/v2/oauth2/authorize" diff --git a/tests/unit/helpers/test_auth_flow_managers.py b/tests/unit/helpers/test_auth_flow_managers.py index 477584f63..b8963f3bf 100644 --- a/tests/unit/helpers/test_auth_flow_managers.py +++ b/tests/unit/helpers/test_auth_flow_managers.py @@ -10,7 +10,7 @@ from globus_sdk.services.auth.flow_managers.authorization_code import ( GlobusAuthorizationCodeFlowManager, ) -from globus_sdk.services.auth.flow_managers.native_app import make_native_app_challenge +from globus_sdk.services.auth.flow_managers.native_app import _make_native_app_challenge @pytest.mark.parametrize( @@ -23,7 +23,7 @@ ) def test_invalid_native_app_challenge(verifier): with pytest.raises(globus_sdk.GlobusSDKUsageError): - make_native_app_challenge(verifier) + _make_native_app_challenge(verifier) def test_simple_input_native_app_challenge(): @@ -33,7 +33,7 @@ def test_simple_input_native_app_challenge(): .rstrip(b"=") .decode("utf-8") ) - res_verifier, res_challenge = make_native_app_challenge(verifier) + res_verifier, res_challenge = _make_native_app_challenge(verifier) assert res_verifier == verifier assert res_challenge == challenge @@ -51,7 +51,7 @@ def mock_b64encode(b: bytes): monkeypatch.setattr(os, "urandom", mock_urandom) monkeypatch.setattr(base64, "urlsafe_b64encode", mock_b64encode) - verifier, challenge = make_native_app_challenge() + verifier, challenge = _make_native_app_challenge() assert verifier == "abc123" assert challenge == "abc123" From 631df8581c06eb75d71de861b0ffd50e113138a8 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 18 Jun 2025 12:30:01 -0500 Subject: [PATCH 051/176] Update get_identities and JWK methods w/ MISSING --- .../auth/client/confidential_client.py | 5 ++- .../services/auth/client/service_client.py | 41 ++++++++++--------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 97d2e0717..8486a36eb 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -94,7 +94,10 @@ def get_identities( ) query_params = { "usernames": commajoin(usernames), - "provision": str(provision).lower(), + # only specify `provision` if `usernames` is given + "provision": ( + str(provision).lower() if usernames is not MISSING else MISSING + ), "ids": commajoin(ids), **(query_params or {}), } diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 7b8c60fbc..4888d9206 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -175,7 +175,7 @@ def get_openid_configuration(self) -> GlobusHTTPResponse: @t.overload def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any], + openid_configuration: GlobusHTTPResponse | dict[str, t.Any] | MissingType, *, as_pem: t.Literal[True], ) -> RSAPublicKey: ... @@ -183,7 +183,7 @@ def get_jwk( @t.overload def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any], + openid_configuration: GlobusHTTPResponse | dict[str, t.Any] | MissingType, *, as_pem: t.Literal[False], ) -> dict[str, t.Any]: ... @@ -193,7 +193,9 @@ def get_jwk( # this will ideally be resolved in a future SDK version by making this the only copy def get_jwk( self, - openid_configuration: None | GlobusHTTPResponse | dict[str, t.Any] = None, + openid_configuration: ( + GlobusHTTPResponse | dict[str, t.Any] | MissingType + ) = MISSING, *, as_pem: bool = False, ) -> RSAPublicKey | dict[str, t.Any]: @@ -262,8 +264,8 @@ def oauth2_userinfo(self) -> GlobusHTTPResponse: def get_identities( self, *, - usernames: t.Iterable[str] | str | None = None, - ids: t.Iterable[UUIDLike] | UUIDLike | None = None, + usernames: t.Iterable[str] | str | MissingType = MISSING, + ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, provision: bool = False, query_params: dict[str, t.Any] | None = None, ) -> GetIdentitiesResponse: @@ -348,26 +350,27 @@ def get_identities( log.debug("Looking up Globus Auth Identities") - if query_params is None: - query_params = {} - - # if either of these params has a truthy value, stringify it - if usernames: - query_params["usernames"] = commajoin(usernames) - query_params["provision"] = ( - "false" if str(provision).lower() == "false" else "true" - ) - if ids: - query_params["ids"] = commajoin(ids) - - log.debug(f"query_params={query_params}") + query_params = { + "usernames": commajoin(usernames), + "ids": commajoin(ids), + # only specify `provision` if `usernames` is given + "provision": ( + str(provision).lower() if usernames is not MISSING else MISSING + ), + **(query_params or {}), + } - if "usernames" in query_params and "ids" in query_params: + if ( + query_params["usernames"] is not MISSING + and query_params["ids"] is not MISSING + ): log.warning( "get_identities call with both usernames and " "identities set! Expected to result in errors" ) + log.debug(f"query_params={query_params}") + return GetIdentitiesResponse( self.get("/v2/api/identities", query_params=query_params) ) From 7aff75335bf435e8c1ff0d4a261c4fe02a285d17 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 18 Jun 2025 12:36:39 -0500 Subject: [PATCH 052/176] Update project and policy usages w/ MISSING Also includes a minor fix for usage which was missing `strseq_listify()` usage. --- .../services/auth/client/service_client.py | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 4888d9206..b10cba42e 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -378,8 +378,8 @@ def get_identities( def get_identity_providers( self, *, - domains: t.Iterable[str] | str | None = None, - ids: t.Iterable[UUIDLike] | UUIDLike | None = None, + domains: t.Iterable[str] | str | MissingType = MISSING, + ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> GetIdentityProvidersResponse: r""" @@ -444,28 +444,23 @@ def get_identity_providers( log.debug("Looking up Globus Auth Identity Providers") - if query_params is None: - query_params = {} - - if domains is not None and ids is not None: + if domains is not MISSING and ids is not MISSING: raise exc.GlobusSDKUsageError( "AuthClient.get_identity_providers does not take both " "'domains' and 'ids'. These are mutually exclusive." ) - # if either of these params has a truthy value, stringify it - # this handles lists of values as well as individual values gracefully - # letting us consume args whose `__str__` methods produce "the right - # thing" - elif domains is not None: - query_params["domains"] = commajoin(domains) - elif ids is not None: - query_params["ids"] = commajoin(ids) - else: + elif domains is MISSING and ids is MISSING: log.warning( "neither 'domains' nor 'ids' provided to get_identity_providers(). " "This can only succeed if 'query_params' were given." ) + query_params = { + "domains": commajoin(domains), + "ids": commajoin(ids), + **(query_params or {}), + } + log.debug(f"query_params={query_params}") return GetIdentityProvidersResponse( self.get("/v2/api/identity_providers", query_params=query_params) @@ -576,8 +571,8 @@ def create_project( display_name: str, contact_email: str, *, - admin_ids: UUIDLike | t.Iterable[UUIDLike] | None = None, - admin_group_ids: UUIDLike | t.Iterable[UUIDLike] | None = None, + admin_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, + admin_group_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Create a new project. Requires the ``manage_projects`` scope. @@ -622,24 +617,22 @@ def create_project( .. extdoclink:: Create Project :ref: auth/reference/#create_project """ - body: dict[str, t.Any] = { + body = { "display_name": display_name, "contact_email": contact_email, + "admin_ids": strseq_listify(admin_ids), + "admin_group_ids": strseq_listify(admin_group_ids), } - if admin_ids is not None: - body["admin_ids"] = strseq_listify(admin_ids) - if admin_group_ids is not None: - body["admin_group_ids"] = strseq_listify(admin_group_ids) return self.post("/v2/api/projects", data={"project": body}) def update_project( self, project_id: UUIDLike, *, - display_name: str | None = None, - contact_email: str | None = None, - admin_ids: UUIDLike | t.Iterable[UUIDLike] | None = None, - admin_group_ids: UUIDLike | t.Iterable[UUIDLike] | None = None, + display_name: str | MissingType = MISSING, + contact_email: str | MissingType = MISSING, + admin_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, + admin_group_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, ) -> GlobusHTTPResponse: """ Update a project. Requires the ``manage_projects`` scope. @@ -677,15 +670,12 @@ def update_project( .. extdoclink:: Update Project :ref: auth/reference/#update_project """ - body: dict[str, t.Any] = {} - if display_name is not None: - body["display_name"] = display_name - if contact_email is not None: - body["contact_email"] = contact_email - if admin_ids is not None: - body["admin_ids"] = strseq_listify(admin_ids) - if admin_group_ids is not None: - body["admin_group_ids"] = strseq_listify(admin_group_ids) + body = { + "display_name": display_name, + "contact_email": contact_email, + "admin_ids": strseq_listify(admin_ids), + "admin_group_ids": strseq_listify(admin_group_ids), + } return self.put(f"/v2/api/projects/{project_id}", data={"project": body}) def delete_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: @@ -888,8 +878,8 @@ def create_policy( # pylint: disable=missing-param-doc "required_mfa": required_mfa, "display_name": display_name, "description": description, - "domain_constraints_include": domain_constraints_include, - "domain_constraints_exclude": domain_constraints_exclude, + "domain_constraints_include": strseq_listify(domain_constraints_include), + "domain_constraints_exclude": strseq_listify(domain_constraints_exclude), } return self.post("/v2/api/policies", data={"policy": body}) @@ -949,8 +939,8 @@ def update_policy( "required_mfa": required_mfa, "display_name": display_name, "description": description, - "domain_constraints_include": domain_constraints_include, - "domain_constraints_exclude": domain_constraints_exclude, + "domain_constraints_include": strseq_listify(domain_constraints_include), + "domain_constraints_exclude": strseq_listify(domain_constraints_exclude), "project_id": project_id, } return self.put(f"/v2/api/policies/{policy_id}", data={"policy": body}) From 306bf99274f52c929d9d234b55ce071a1395dc99 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 18 Jun 2025 12:54:02 -0500 Subject: [PATCH 053/176] Fix errors from AuthClient MISSING updates - "unreachable code" due to None vs MISSING in a comparison - tests which expect None handling should expect MISSING handling - one test which tested *unsupported* values (by typing contract) has been narrowed to supported types only --- src/globus_sdk/services/auth/client/service_client.py | 2 +- .../auth/service_client/test_get_identities.py | 10 +++++----- .../auth/service_client/test_update_project.py | 11 ++++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index b10cba42e..f0630e93d 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -210,7 +210,7 @@ def get_jwk( :param as_pem: Decode the JWK to an RSA PEM key, typically for JWT decoding :type as_pem: bool """ - if openid_configuration is None: + if isinstance(openid_configuration, MissingType): log.debug("No OIDC Config provided, autofetching...") openid_configuration = self.get_openid_configuration() jwk_data = get_jwk_data( diff --git a/tests/functional/services/auth/service_client/test_get_identities.py b/tests/functional/services/auth/service_client/test_get_identities.py index 1ecb47b2b..93a705942 100644 --- a/tests/functional/services/auth/service_client/test_get_identities.py +++ b/tests/functional/services/auth/service_client/test_get_identities.py @@ -55,15 +55,15 @@ def test_get_identities_success(usernames, service_client): [ (True, "true"), (False, "false"), - (1, "true"), - (0, "true"), - ("fALSe", "false"), - ("true", "true"), + (None, "false"), ], ) def test_get_identities_provision(inval, outval, service_client): load_response(service_client.get_identities) - service_client.get_identities(usernames="globus@globus.org", provision=inval) + if inval is not None: + service_client.get_identities(usernames="globus@globus.org", provision=inval) + else: + service_client.get_identities(usernames="globus@globus.org") lastreq = get_last_request() assert "provision" in lastreq.params assert lastreq.params["provision"] == outval diff --git a/tests/functional/services/auth/service_client/test_update_project.py b/tests/functional/services/auth/service_client/test_update_project.py index 129bcf6cb..c755dbd32 100644 --- a/tests/functional/services/auth/service_client/test_update_project.py +++ b/tests/functional/services/auth/service_client/test_update_project.py @@ -3,17 +3,18 @@ import pytest +from globus_sdk._missing import MISSING, filter_missing from globus_sdk._testing import get_last_request, load_response @pytest.mark.parametrize( - "admin_id_style", ("none", "string", "list", "set", "uuid", "uuid_list") + "admin_id_style", ("missing", "string", "list", "set", "uuid", "uuid_list") ) def test_update_project_admin_id_styles(service_client, admin_id_style): meta = load_response(service_client.update_project).metadata - if admin_id_style == "none": - admin_ids = None + if admin_id_style == "missing": + admin_ids = MISSING elif admin_id_style == "string": admin_ids = meta["admin_id"] elif admin_id_style == "list": @@ -37,8 +38,8 @@ def test_update_project_admin_id_styles(service_client, admin_id_style): last_req = get_last_request() data = json.loads(last_req.body) assert list(data) == ["project"], data # 'project' is the only key - if admin_id_style == "none": - assert data["project"] == {"display_name": "My Project"} + if admin_id_style == "missing": + assert filter_missing(data["project"]) == {"display_name": "My Project"} else: assert data["project"] == { "display_name": "My Project", From 50fed2f8904f5e8e32346406c7742dfaaa085fb2 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 18 Jun 2025 13:03:01 -0500 Subject: [PATCH 054/176] Typo fix: double-conversion of a value --- src/globus_sdk/login_flows/login_flow_manager.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/globus_sdk/login_flows/login_flow_manager.py b/src/globus_sdk/login_flows/login_flow_manager.py index 7847215e0..548b96256 100644 --- a/src/globus_sdk/login_flows/login_flow_manager.py +++ b/src/globus_sdk/login_flows/login_flow_manager.py @@ -52,15 +52,14 @@ def _get_authorize_url( """ self._oauth2_start_flow(auth_parameters, redirect_uri) - session_required_single_domain = none2missing( - auth_parameters.session_required_single_domain - ) prompt = none2missing(auth_parameters.prompt) return self.login_client.oauth2_get_authorize_url( session_required_identities=none2missing( auth_parameters.session_required_identities ), - session_required_single_domain=none2missing(session_required_single_domain), + session_required_single_domain=none2missing( + auth_parameters.session_required_single_domain + ), session_required_policies=none2missing( auth_parameters.session_required_policies ), From 92a0e00094b4b53a9e74817670ee2606bafef87f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 23 Jun 2025 11:29:23 -0500 Subject: [PATCH 055/176] Add changelog for Auth client MISSING conversion --- .../20250623_112759_sirosen_auth_missing_defaults.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst diff --git a/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst b/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst new file mode 100644 index 000000000..2968ca9ba --- /dev/null +++ b/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst @@ -0,0 +1,5 @@ +Breaking Changes +---------------- + +- In Globus Auth client classes, defaults of ``None`` are converted to + ``MISSING`` for optional fields. (:pr:`NUMBER`) From b96ec004d4ec68400a9a8374a7a0262713ffc90f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 23 Jun 2025 13:12:48 -0500 Subject: [PATCH 056/176] Apply suggestions from code review Co-authored-by: Kurt McKee --- .../20250620_104615_sirosen_remove_transfer_client_param.rst | 5 ++--- docs/upgrading.rst | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst b/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst index 3002f4297..3c9ba1177 100644 --- a/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst +++ b/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst @@ -1,6 +1,5 @@ Breaking Changes ---------------- -- Support for ``transfer_client`` as a parameter to ``TransferData`` and - ``DeleteData`` has been removed. See the upgrading doc for transition - details. (:pr:`NUMBER`) +- The ``transfer_client`` parameter to ``TransferData`` and ``DeleteData`` has been removed. + See the upgrading doc for transition details. (:pr:`NUMBER`) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 110848750..44abbdf8d 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -48,7 +48,7 @@ From 3.x to 4.0 ``TransferData`` and ``DeleteData`` Do Not Take a ``TransferClient`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The signatures for these two data constructors has changed to remove support +The signatures for these two data constructors have changed to remove support for ``transfer_client`` as their first parameter. Generally, update usage which passed a client to omit it: @@ -95,7 +95,7 @@ The client object was used to fetch a ``submission_id`` on initialization. Users typically will rely on ``TransferClient.submit_transfer()`` and ``TransferClient.submit_delete()`` filling in this value. To control when a submission ID is fetched, use -``TransferClient.get_submsission_id()``, as in: +``TransferClient.get_submission_id()``, as in: .. code-block:: python From c1958122f1339bbb5436b763e2c3eb9864974850 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 23 Jun 2025 13:23:26 -0500 Subject: [PATCH 057/176] Remove unnecessary backslash continuations Co-authored-by: Kurt McKee <39996+kurtmckee@users.noreply.github.com> --- src/globus_sdk/services/transfer/data/delete_data.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index f1a28cd59..3e28825e3 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -23,9 +23,9 @@ class DeleteData(GlobusPayload): :param endpoint: The endpoint ID which is targeted by this deletion Task :param label: A string label for the Task - :param submission_id: A submission ID value fetched via :meth:`get_submission_id \ + :param submission_id: A submission ID value fetched via :meth:`get_submission_id `. By default, the SDK - will fetch and populate this field when :meth:`submit_delete \ + will fetch and populate this field when :meth:`submit_delete ` is called. :param recursive: Recursively delete subdirectories on the target endpoint [default: ``False``] @@ -65,10 +65,10 @@ class DeleteData(GlobusPayload): **External Documentation** See the - `Task document definition \ + `Task document definition `_ and - `Delete specific fields \ + `Delete specific fields `_ in the REST documentation for more details on Delete Task documents. From bfa385a2a137e3d6993a9ebd6b9889b80638528f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 23 Jun 2025 13:35:17 -0500 Subject: [PATCH 058/176] Apply suggestions from code review Co-authored-by: Kurt McKee --- src/globus_sdk/services/auth/client/base_login_client.py | 2 +- src/globus_sdk/services/auth/client/confidential_client.py | 5 +---- src/globus_sdk/services/auth/client/service_client.py | 6 +++--- src/globus_sdk/services/auth/flow_managers/native_app.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 194cfa55a..6e666eaec 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -395,7 +395,7 @@ def oauth2_token( log.debug("Fetching new token from Globus Auth") # use the fact that requests implicitly encodes the `data` parameter as # a form POST - data = {**dict(form_data), **(body_params or {})} + data = {**form_data, **(body_params or {})} return response_class( self.post( "/v2/oauth2/token", diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 8486a36eb..c6fdb871a 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -451,7 +451,7 @@ def create_child_client( "terms_and_conditions and privacy_policy must both be set or unset" ) links: dict[str, str | MissingType] | MissingType = MISSING - if terms_and_conditions or privacy_policy: + if terms_and_conditions and privacy_policy: links = { "terms_and_conditions": terms_and_conditions, "privacy_policy": privacy_policy, @@ -469,7 +469,4 @@ def create_child_client( **(additional_fields or {}), } - if additional_fields is not None: - body.update(additional_fields) - return self.post("/v2/api/clients", data={"client": body}) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index f0630e93d..78ae68f44 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -365,8 +365,8 @@ def get_identities( and query_params["ids"] is not MISSING ): log.warning( - "get_identities call with both usernames and " - "identities set! Expected to result in errors" + "get_identities called with both usernames and " + "identities set! Expecting an error." ) log.debug(f"query_params={query_params}") @@ -451,7 +451,7 @@ def get_identity_providers( ) elif domains is MISSING and ids is MISSING: log.warning( - "neither 'domains' nor 'ids' provided to get_identity_providers(). " + "Neither 'domains' nor 'ids' provided to get_identity_providers(). " "This can only succeed if 'query_params' were given." ) diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index 963b55f31..423deda30 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -185,8 +185,8 @@ def get_authorize_url(self, query_params: dict[str, t.Any] | None = None) -> str "code_challenge_method": "S256", "access_type": (self.refresh_tokens and "offline") or "online", "prefill_named_grant": self.prefill_named_grant, + **(query_params or {}), } - params.update(query_params or {}) params = filter_missing(params) encoded_params = urllib.parse.urlencode(params) From 3bfaf164056441e063d9d6800b50f2f5501d35eb Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 23 Jun 2025 13:40:34 -0500 Subject: [PATCH 059/176] Make tests of `provision` flag clearer --- .../service_client/test_get_identities.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/functional/services/auth/service_client/test_get_identities.py b/tests/functional/services/auth/service_client/test_get_identities.py index 93a705942..2efc29bfb 100644 --- a/tests/functional/services/auth/service_client/test_get_identities.py +++ b/tests/functional/services/auth/service_client/test_get_identities.py @@ -50,20 +50,18 @@ def test_get_identities_success(usernames, service_client): } -@pytest.mark.parametrize( - "inval, outval", - [ - (True, "true"), - (False, "false"), - (None, "false"), - ], -) -def test_get_identities_provision(inval, outval, service_client): +def test_get_identities_provision_flag_defaults_to_false(service_client): load_response(service_client.get_identities) - if inval is not None: - service_client.get_identities(usernames="globus@globus.org", provision=inval) - else: - service_client.get_identities(usernames="globus@globus.org") + service_client.get_identities(usernames="globus@globus.org") + lastreq = get_last_request() + assert "provision" in lastreq.params + assert lastreq.params["provision"] == "false" + + +@pytest.mark.parametrize("inval, outval", [(True, "true"), (False, "false")]) +def test_get_identities_provision_flag_formatting(inval, outval, service_client): + load_response(service_client.get_identities) + service_client.get_identities(usernames="globus@globus.org", provision=inval) lastreq = get_last_request() assert "provision" in lastreq.params assert lastreq.params["provision"] == outval From 93514eb45b2cb60679cda288b0a9e7a55f020639 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 3 Jun 2025 18:33:27 -0500 Subject: [PATCH 060/176] Separate the ScopeParser from Scope - Define a new type, ScopeParser - Move parse and merge_scopes to ScopeParser as classmethods - Remove serialize/deserialize as Scope methods - Make Scope into a dataclass - Update Scope.parse to replace deserialize - Move the `_parser` module to `_graph_parser` to provide a clearer separation to SDK maintainers, who can now find `ScopeParser` in a dedicated `parser` module - Move scope graph loading to `ScopeGraph.parse` In the course of this refactor, `ScopeParser.parse()` is also reformulated in a way which will be more suitable for immutable Scope objects, by constructing the tree of scope objects "bottom up" rather than "top down". In service of this rearrangement, a BFS traversal method is added to the ScopeGraph IR. --- ...0603_181734_sirosen_split_scope_parser.rst | 17 ++ .../scopes_and_consents/index.rst | 1 + .../scopes_and_consents/scope_parsing.rst | 22 ++ .../scopes_and_consents/scopes.rst | 24 +- src/globus_sdk/globus_app/app.py | 5 +- src/globus_sdk/scopes/__init__.py | 2 + .../scopes/{_parser.py => _graph_parser.py} | 287 +++++++++--------- src/globus_sdk/scopes/_normalize.py | 5 +- src/globus_sdk/scopes/consents/_model.py | 7 +- src/globus_sdk/scopes/parser.py | 74 +++++ src/globus_sdk/scopes/representation.py | 98 ++---- tests/benchmark/test_scope_parser.py | 6 +- tests/common/consents.py | 4 +- tests/unit/scopes/test_merge_scopes.py | 22 +- tests/unit/scopes/test_scope_parser.py | 26 +- ...ope_parser_intermediate_representations.py | 10 +- .../v2/test_validating_token_storage.py | 4 +- 17 files changed, 337 insertions(+), 277 deletions(-) create mode 100644 changelog.d/20250603_181734_sirosen_split_scope_parser.rst create mode 100644 docs/authorization/scopes_and_consents/scope_parsing.rst rename src/globus_sdk/scopes/{_parser.py => _graph_parser.py} (83%) create mode 100644 src/globus_sdk/scopes/parser.py diff --git a/changelog.d/20250603_181734_sirosen_split_scope_parser.rst b/changelog.d/20250603_181734_sirosen_split_scope_parser.rst new file mode 100644 index 000000000..e08042148 --- /dev/null +++ b/changelog.d/20250603_181734_sirosen_split_scope_parser.rst @@ -0,0 +1,17 @@ +Changed +~~~~~~~ + +- Scope parsing has been separated from the main ``Scope`` class into a + dedicated ``ScopeParser`` which provides parsing methods. (:pr:`NUMBER`) + + - Use ``globus_sdk.scopes.ScopeParser`` for complex parsing use-cases. The + ``ScopeParser.parse`` classmethod parses strings into lists of scope + objects. + + - ``Scope.serialize`` and ``Scope.deserialize`` have been removed as methods. + + - ``Scope.parse`` is changed to call ``ScopeParser.parse`` and verify that + there is exactly one result, which it returns. This means that + ``Scope.parse`` now returns a single ``Scope``, not a ``list[Scope]``. + + - ``Scope.merge_scopes`` has been moved to ``ScopeParser.merge_scopes``. diff --git a/docs/authorization/scopes_and_consents/index.rst b/docs/authorization/scopes_and_consents/index.rst index 03998e1a7..0bbf07844 100644 --- a/docs/authorization/scopes_and_consents/index.rst +++ b/docs/authorization/scopes_and_consents/index.rst @@ -31,3 +31,4 @@ which make learning about and manipulating these data easier. scopes consents + scope_parsing diff --git a/docs/authorization/scopes_and_consents/scope_parsing.rst b/docs/authorization/scopes_and_consents/scope_parsing.rst new file mode 100644 index 000000000..f3dc91f8b --- /dev/null +++ b/docs/authorization/scopes_and_consents/scope_parsing.rst @@ -0,0 +1,22 @@ +.. _scope_parsing: + +.. currentmodule:: globus_sdk.scopes + +Scope Parsing +============= + +Scope parsing is handled by the :class:`ScopeParser` type. + +Additionally, :class:`Scope` objects define a +:meth:`parse() ` method which wraps parser +usage. + +:class:`ScopeParser` provides classmethods as its primary interface, so there +is no need to instantiate the parser in order to use it. + +ScopeParser Reference +--------------------- + +.. autoclass:: ScopeParser + :members: + :show-inheritance: diff --git a/docs/authorization/scopes_and_consents/scopes.rst b/docs/authorization/scopes_and_consents/scopes.rst index 193185fe0..624eb860a 100644 --- a/docs/authorization/scopes_and_consents/scopes.rst +++ b/docs/authorization/scopes_and_consents/scopes.rst @@ -112,24 +112,21 @@ The SDK provides a ``Scope`` object which is the class model for a scope. ``Scope``\s can be parsed from strings and serialized to strings, and support programmatic manipulations to describe dependent scopes. -``Scope`` can be constructed using its initializer, or one of its two main -parsing methods: ``Scope.parse`` and ``Scope.deserialize``. -``parse`` produces a list of scopes from a string, while ``deserialize`` -produces exactly one. +``Scope`` can be constructed using its initializer, via ``Scope.parse``, or via +:meth:`ScopeParser.parse`. -For example, one can create a ``Scope`` from the Groups "all" scope -as follows: +For example, one can create a ``Scope`` object for the OIDC ``openid`` scope: .. code-block:: python - from globus_sdk.scopes import GroupsScopes, Scope + from globus_sdk.scopes import Scope - group_scope = Scope.deserialize(GroupsScopes.all) + openid_scope = Scope("openid") ``Scope`` objects primarily provide three main pieces of functionality: - * parsing (deserializing) - * stringifying (serializing) + * deserializing (parsing a single scope) + * serializing (stringifying) * scope tree construction Scope Construction @@ -162,9 +159,8 @@ Serializing Scopes Whenever scopes are being sent to Globus services, they need to be encoded as strings. All scope objects support this by means of their defined -``serialize`` method. Note that ``__str__`` for a ``Scope`` is just an -alias for ``serialize``. For example, the following is an example of -``str()``, ``repr()``, and ``serialize()`` usage: +``__str__`` method. For example, the following is an example of +``str()`` and ``repr()`` usage: .. code-block:: pycon @@ -175,7 +171,7 @@ alias for ``serialize``. For example, the following is an example of >>> foo.add_dependency(bar) >>> print(str(foo)) foo[bar[baz]] - >>> print(bar.serialize()) + >>> print(str(bar)) bar[baz] >>> alpha = Scope("alpha") >>> alpha.add_dependency("beta", optional=True) diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index b92427b36..674c63e47 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -10,12 +10,11 @@ AuthLoginClient, GlobusSDKUsageError, IDTokenDecoder, - Scope, ) from globus_sdk._types import ScopeCollectionType, UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.gare import GlobusAuthorizationParameters -from globus_sdk.scopes import AuthScopes, scopes_to_scope_list +from globus_sdk.scopes import AuthScopes, Scope, ScopeParser, scopes_to_scope_list from globus_sdk.tokenstorage import ( ScopeRequirementsValidator, TokenStorage, @@ -383,7 +382,7 @@ def _auth_params_with_required_scopes( # merge scopes for deduplication to minimize url request length # this is useful even if there weren't any auth_param scope requirements # as the app's scope_requirements can have duplicates - combined_scopes = Scope.merge_scopes( + combined_scopes = ScopeParser.merge_scopes( required_scopes, [Scope(s) for s in auth_params.required_scopes or []] ) auth_params.required_scopes = [str(s) for s in combined_scopes] diff --git a/src/globus_sdk/scopes/__init__.py b/src/globus_sdk/scopes/__init__.py index 43401d529..93c53b24e 100644 --- a/src/globus_sdk/scopes/__init__.py +++ b/src/globus_sdk/scopes/__init__.py @@ -14,11 +14,13 @@ TransferScopes, ) from .errors import ScopeCycleError, ScopeParseError +from .parser import ScopeParser from .representation import Scope __all__ = ( "ScopeBuilder", "Scope", + "ScopeParser", "ScopeParseError", "ScopeCycleError", "GCSCollectionScopeBuilder", diff --git a/src/globus_sdk/scopes/_parser.py b/src/globus_sdk/scopes/_graph_parser.py similarity index 83% rename from src/globus_sdk/scopes/_parser.py rename to src/globus_sdk/scopes/_graph_parser.py index 8d05fc0b9..b3873cb03 100644 --- a/src/globus_sdk/scopes/_parser.py +++ b/src/globus_sdk/scopes/_graph_parser.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import typing as t from collections import defaultdict, deque @@ -9,6 +10,150 @@ SPECIAL_TOKENS = set("[]*") +class ScopeGraph: + def __init__(self) -> None: + self.top_level_scopes: set[tuple[str, bool]] = set() + self.nodes: set[str] = set() + self.edges: set[tuple[str, str, bool]] = set() + self.adjacency_matrix: dict[str, set[tuple[str, str, bool]]] = defaultdict(set) + + def breadth_first_walk(self) -> t.Iterator[tuple[str, bool]]: + """ + Do a BFS across the forest, returning nodes (as tuples) in BFS order. + + For inspection of the graph after parsing. + """ + bfs_queue: t.Deque[tuple[str, bool]] = deque(self.top_level_scopes) + + while bfs_queue: + yield (current := bfs_queue.popleft()) + + edges = self.adjacency_matrix[current[0]] + for _, dest, optional in edges: + bfs_queue.append((dest, optional)) + + def add_edge(self, src: str, dest: str, optional: bool) -> None: + self.edges.add((src, dest, optional)) + self.adjacency_matrix[src].add((src, dest, optional)) + + def _normalize_optionals(self) -> None: + to_remove: set[tuple[str, str, bool]] = set() + for edge in self.edges: + src, dest, optional = edge + if not optional: + continue + alter_ego = (src, dest, not optional) + if alter_ego in self.edges: + to_remove.add(edge) + self.edges = self.edges - to_remove + for edge in to_remove: + src, _, _ = edge + self.adjacency_matrix[src].remove(edge) + + def _check_cycles(self) -> None: + # explore the graph using an iterative Depth-First Search + # as we explore the graph, keep track of paths of ancestry being explored + # if we ever find a back-edge along one of those paths of ancestry, that + # means that there must be a cycle + + # start from the top-level nodes (which we know to be the roots of this + # forest-shaped graph) + # we will track this as the set of paths to continue to branch and explore in a + # stack and pop from it until it is empty, thus implementing DFS + # + # conceptually, the paths could be implemented as `list[str]`, which would + # preserve the order in which we encountered each node. Using a set is a + # micro-optimization which makes checks faster, since we only care to detect + # *that* there was a cycle, not what the shape of that cycle was + paths_to_explore: list[tuple[set[str], str]] = [ + ({node}, node) for node, _ in self.top_level_scopes + ] + + while paths_to_explore: + path, terminus = paths_to_explore.pop() + + # get out-edges from the last node in the path + children = self.adjacency_matrix[terminus] + + # if the node was a leaf, no children, we are done exploring this path + if not children: + continue + + # for each child edge, do two basic things: + # - check if we found a back-edge (cycle!) + # - create a new path to explore, with the child node as its current + # terminus + for edge in children: + _, dest, _ = edge + if dest in path: + raise ScopeCycleError(f"A cycle was found involving '{dest}'") + paths_to_explore.append((path.union((dest,)), dest)) + + def __str__(self) -> str: + lines = ["digraph scopes {", ' rankdir="LR";', ""] + for node, optional in self.top_level_scopes: + lines.append(f" {'*' if optional else ''}{node}") + lines.append("") + + # do two passes to put all non-optional edges first + for source, dest, optional in self.edges: + if optional: + continue + lines.append(f" {source} -> {dest};") + for source, dest, optional in self.edges: + if not optional: + continue + lines.append(f' {source} -> {dest} [ label = "optional" ];') + lines.append("") + lines.append("}") + return "\n".join(lines) + + @classmethod + def parse(cls, scopes: str) -> ScopeGraph: + trees = ScopeTreeNode.parse(scopes) + graph = cls._convert_trees(trees) + graph._normalize_optionals() + graph._check_cycles() + return graph + + @classmethod + def _convert_trees(cls, trees: list[ScopeTreeNode]) -> ScopeGraph: + graph = ScopeGraph() + node_queue: t.Deque[ScopeTreeNode] = deque() + + for tree_node in trees: + node_queue.append(tree_node) + graph.top_level_scopes.add((tree_node.scope_string, tree_node.optional)) + + while node_queue: + tree_node = node_queue.pop() + scope_string = tree_node.scope_string + graph.nodes.add(scope_string) + for dep in tree_node.dependencies: + node_queue.append(dep) + graph.add_edge(scope_string, dep.scope_string, dep.optional) + + return graph + + +@dataclasses.dataclass(slots=True) +class ScopeTreeNode: + # + # This is an intermediate representation for scope parsing. + # + scope_string: str + optional: bool + dependencies: list[ScopeTreeNode] = dataclasses.field(default_factory=list) + + def add_dependency(self, subtree: ScopeTreeNode) -> None: + self.dependencies.append(subtree) + + @staticmethod + def parse(scope_string: str) -> list[ScopeTreeNode]: + tokens = _tokenize(scope_string) + return _parse_tokens(tokens) + + def _tokenize(scope_string: str) -> list[str]: tokens: list[str] = [] start = 0 @@ -105,140 +250,6 @@ def _reject_bad_adjacent_tokens(current_token: str, next_token: str | None) -> N raise ScopeParseError("found double left-bracket") -class ScopeTreeNode: - # - # This is an intermediate representation for scope parsing. - # - def __init__( - self, - scope_string: str, - *, - optional: bool, - ) -> None: - self.scope_string = scope_string - self.optional = optional - self.dependencies: list[ScopeTreeNode] = [] - - def add_dependency(self, subtree: ScopeTreeNode) -> None: - self.dependencies.append(subtree) - - def __repr__(self) -> str: - parts: list[str] = [f"'{self.scope_string}'"] - if self.optional: - parts.append("optional=True") - if self.dependencies: - parts.append(f"dependencies={self.dependencies!r}") - return "ScopeTreeNode(" + ", ".join(parts) + ")" - - @staticmethod - def parse(scope_string: str) -> list[ScopeTreeNode]: - tokens = _tokenize(scope_string) - return _parse_tokens(tokens) - - -class ScopeGraph: - def __init__(self) -> None: - self.top_level_scopes: set[tuple[str, bool]] = set() - self.nodes: set[str] = set() - self.edges: set[tuple[str, str, bool]] = set() - self.adjacency_matrix: dict[str, set[tuple[str, str, bool]]] = defaultdict(set) - - def add_edge(self, src: str, dest: str, optional: bool) -> None: - self.edges.add((src, dest, optional)) - self.adjacency_matrix[src].add((src, dest, optional)) - - def _normalize_optionals(self) -> None: - to_remove: set[tuple[str, str, bool]] = set() - for edge in self.edges: - src, dest, optional = edge - if not optional: - continue - alter_ego = (src, dest, not optional) - if alter_ego in self.edges: - to_remove.add(edge) - self.edges = self.edges - to_remove - for edge in to_remove: - src, _, _ = edge - self.adjacency_matrix[src].remove(edge) - - def _check_cycles(self) -> None: - # explore the graph using an iterative Depth-First Search - # as we explore the graph, keep track of paths of ancestry being explored - # if we ever find a back-edge along one of those paths of ancestry, that - # means that there must be a cycle - - # start from the top-level nodes (which we know to be the roots of this - # forest-shaped graph) - # we will track this as the set of paths to continue to branch and explore in a - # stack and pop from it until it is empty, thus implementing DFS - # - # conceptually, the paths could be implemented as `list[str]`, which would - # preserve the order in which we encountered each node. Using a set is a - # micro-optimization which makes checks faster, since we only care to detect - # *that* there was a cycle, not what the shape of that cycle was - paths_to_explore: list[tuple[set[str], str]] = [ - ({node}, node) for node, _ in self.top_level_scopes - ] - - while paths_to_explore: - path, terminus = paths_to_explore.pop() - - # get out-edges from the last node in the path - children = self.adjacency_matrix[terminus] - - # if the node was a leaf, no children, we are done exploring this path - if not children: - continue - - # for each child edge, do two basic things: - # - check if we found a back-edge (cycle!) - # - create a new path to explore, with the child node as its current - # terminus - for edge in children: - _, dest, _ = edge - if dest in path: - raise ScopeCycleError(f"A cycle was found involving '{dest}'") - paths_to_explore.append((path.union((dest,)), dest)) - - def __str__(self) -> str: - lines = ["digraph scopes {", ' rankdir="LR";', ""] - for node, optional in self.top_level_scopes: - lines.append(f" {'*' if optional else ''}{node}") - lines.append("") - - # do two passes to put all non-optional edges first - for source, dest, optional in self.edges: - if optional: - continue - lines.append(f" {source} -> {dest};") - for source, dest, optional in self.edges: - if not optional: - continue - lines.append(f' {source} -> {dest} [ label = "optional" ];') - lines.append("") - lines.append("}") - return "\n".join(lines) - - -def _convert_trees(trees: list[ScopeTreeNode]) -> ScopeGraph: - graph = ScopeGraph() - node_queue: t.Deque[ScopeTreeNode] = deque() - - for tree_node in trees: - node_queue.append(tree_node) - graph.top_level_scopes.add((tree_node.scope_string, tree_node.optional)) - - while node_queue: - tree_node = node_queue.pop() - scope_string = tree_node.scope_string - graph.nodes.add(scope_string) - for dep in tree_node.dependencies: - node_queue.append(dep) - graph.add_edge(scope_string, dep.scope_string, dep.optional) - - return graph - - def _peek_enumerate(data: str | list[str]) -> t.Iterator[tuple[int, str, str | None]]: """ An iterator producing (index, character, next_char) @@ -255,11 +266,3 @@ def _peek_enumerate(data: str | list[str]) -> t.Iterator[tuple[int, str, str | N prev = c yield (len(data) - 1, prev, None) - - -def parse_scope_graph(scopes: str) -> ScopeGraph: - trees = ScopeTreeNode.parse(scopes) - graph = _convert_trees(trees) - graph._normalize_optionals() - graph._check_cycles() - return graph diff --git a/src/globus_sdk/scopes/_normalize.py b/src/globus_sdk/scopes/_normalize.py index f84c968e4..e88112913 100644 --- a/src/globus_sdk/scopes/_normalize.py +++ b/src/globus_sdk/scopes/_normalize.py @@ -2,6 +2,7 @@ import typing as t +from .parser import ScopeParser from .representation import Scope if t.TYPE_CHECKING: @@ -47,7 +48,7 @@ def scopes_to_scope_list(scopes: ScopeCollectionType) -> list[Scope]: scope_list: list[Scope] = [] for scope in _iter_scope_collection(scopes): if isinstance(scope, str): - scope_list.extend(Scope.parse(scope)) + scope_list.extend(ScopeParser.parse(scope)) else: scope_list.append(scope) return scope_list @@ -99,5 +100,5 @@ def _iter_scope_string(scope_str: str, split_root_scopes: bool) -> t.Iterator[st elif "[" not in scope_str: yield from scope_str.split(" ") else: - for scope_obj in Scope.parse(scope_str): + for scope_obj in ScopeParser.parse(scope_str): yield str(scope_obj) diff --git a/src/globus_sdk/scopes/consents/_model.py b/src/globus_sdk/scopes/consents/_model.py index abd137329..4c64a25a5 100644 --- a/src/globus_sdk/scopes/consents/_model.py +++ b/src/globus_sdk/scopes/consents/_model.py @@ -32,6 +32,7 @@ from globus_sdk._types import UUIDLike +from ..parser import ScopeParser from ..representation import Scope from ._errors import ConsentParseError, ConsentTreeConstructionError @@ -313,7 +314,7 @@ def _normalize_scope_types( """ Normalize the input scope types into a list of Scope objects. - Strings are parsed into 1 or more Scopes using `Scope.parse`. + Strings are parsed into 1 or more Scopes using `ScopeParser.parse`. :param scopes: Some collection of 0 or more scopes as Scope or scope strings. :returns: A list of Scope objects. @@ -322,12 +323,12 @@ def _normalize_scope_types( if isinstance(scopes, Scope): return [scopes] elif isinstance(scopes, str): - return Scope.parse(scopes) + return ScopeParser.parse(scopes) else: scope_list = [] for scope in scopes: if isinstance(scope, str): - scope_list.extend(Scope.parse(scope)) + scope_list.extend(ScopeParser.parse(scope)) else: scope_list.append(scope) return scope_list diff --git a/src/globus_sdk/scopes/parser.py b/src/globus_sdk/scopes/parser.py new file mode 100644 index 000000000..476b0dae4 --- /dev/null +++ b/src/globus_sdk/scopes/parser.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from ._graph_parser import ScopeGraph +from .representation import Scope + + +class ScopeParser: + """ + The ``ScopeParser`` handles the conversion of strings to scopes. + + Most interfaces are classmethods, meaning users should prefer usage like + ``ScopeParser.parse("foo")`` + """ + + @classmethod + def parse(cls, scope_string: str) -> list[Scope]: + """ + Parse an arbitrary scope string to a list of scopes. + + Zero or more than one scope may be returned, as in the case of an empty string + or space-delimited scopes. + + .. warning:: + + Parsing passes through an intermediary representation which treats scopes + as a graph. This ensures that the behavior of parses matches the treatment + of scope strings in Globus Auth authorization flows. + However, this also means that the parsing does not allow for strings which + represent consent trees with structures in which the same scope appears in + multiple parts of the tree. + + :param scope_string: The string to parse + """ + # build the graph intermediate representation + scope_graph = ScopeGraph.parse(scope_string) + + # traverse the graph in a reversed BFS scan + # + # this means we'll handle leaf nodes first, and we'll never reach a + # node before its descendants (dependencies) + # + # as we work, build a lookup table for built Scope objects so that we can + # quickly retrieve elements + built_scopes: dict[tuple[str, bool], Scope] = {} + + for name, optionality in list(scope_graph.breadth_first_walk())[::-1]: + dependencies: list[Scope] = [ + # the lookup in built_scopes here is safe because of the + # reversed BFS ordering + built_scopes[(dep_name, dep_optional)] + for _, dep_name, dep_optional in scope_graph.adjacency_matrix[name] + ] + + built_scopes[(name, optionality)] = Scope( + name, optional=optionality, dependencies=dependencies + ) + + # only return the top-level elements from that build process + # (the roots of the forest-shaped graph) + return [built_scopes[key] for key in scope_graph.top_level_scopes] + + @classmethod + def merge_scopes(cls, scopes_a: list[Scope], scopes_b: list[Scope]) -> list[Scope]: + """ + Given two lists of Scopes, merge them into one list of Scopes by parsing + them as one combined scope string. + + :param scopes_a: list of Scopes to be merged with scopes_b + :param scopes_b: list of Scopes to be merged with scopes_a + """ + # dict of base scope_string: list of scopes with that base scope_string + return cls.parse( + " ".join([str(s) for s in scopes_a] + [str(s) for s in scopes_b]) + ) diff --git a/src/globus_sdk/scopes/representation.py b/src/globus_sdk/scopes/representation.py index 7aabdc168..e89856d9c 100644 --- a/src/globus_sdk/scopes/representation.py +++ b/src/globus_sdk/scopes/representation.py @@ -1,10 +1,10 @@ from __future__ import annotations +import dataclasses import warnings -from ._parser import parse_scope_graph - +@dataclasses.dataclass(slots=True) class Scope: """ A scope object is a representation of a scope which allows modifications to be @@ -18,76 +18,21 @@ class Scope: be declined by the user without declining consent for other scopes """ - def __init__( + scope_string: str + optional: bool = dataclasses.field(default=False) + dependencies: list[Scope] = dataclasses.field(default_factory=list) + + def __post_init__( self, - scope_string: str, - *, - optional: bool = False, - dependencies: list[Scope] | None = None, ) -> None: - if any(c in scope_string for c in "[]* "): + if any(c in self.scope_string for c in "[]* "): raise ValueError( "Scope instances may not contain the special characters '[]* '. " - "Use either Scope.deserialize or Scope.parse instead" + "Use Scope.parse instead." ) - self.scope_string = scope_string - self.optional = optional - self.dependencies: list[Scope] = [] if dependencies is None else dependencies - - @staticmethod - def parse(scope_string: str) -> list[Scope]: - """ - Parse an arbitrary scope string to a list of scopes. - - Zero or more than one scope may be returned, as in the case of an empty string - or space-delimited scopes. - - .. warning:: - - Parsing passes through an intermediary representation which treats scopes - as a graph. This ensures that the behavior of parses matches the treatment - of scope strings in Globus Auth authorization flows. - However, this also means that the parsing does not allow for strings which - represent consent trees with structures in which the same scope appears in - multiple parts of the tree. - - :param scope_string: The string to parse - """ - scope_graph = parse_scope_graph(scope_string) - - # initialize BFS traversals (one per root node) and copy that data - # to setup the result data - bfs_additions: list[Scope] = [ - Scope(s, optional=optional) for s, optional in scope_graph.top_level_scopes - ] - results: list[Scope] = list(bfs_additions) - - while bfs_additions: - current_scope = bfs_additions.pop(0) - edges = scope_graph.adjacency_matrix[current_scope.scope_string] - for _, dest, optional in edges: - dest_scope = Scope(dest, optional=optional) - current_scope.add_dependency(dest_scope) - bfs_additions.append(dest_scope) - - return results - - @staticmethod - def merge_scopes(scopes_a: list[Scope], scopes_b: list[Scope]) -> list[Scope]: - """ - Given two lists of Scopes, merge them into one list of Scopes by parsing - them as one combined scope string. - - :param scopes_a: list of Scopes to be merged with scopes_b - :param scopes_b: list of Scopes to be merged with scopes_a - """ - # dict of base scope_string: list of scopes with that base scope_string - return Scope.parse( - " ".join([str(s) for s in scopes_a] + [str(s) for s in scopes_b]) - ) @classmethod - def deserialize(cls, scope_string: str) -> Scope: + def parse(cls, scope_string: str) -> Scope: """ Deserialize a scope string to a scope object. @@ -97,22 +42,18 @@ def deserialize(cls, scope_string: str) -> Scope: :param scope_string: The string to parse """ - data = Scope.parse(scope_string) + # deferred import because ScopeParser depends on Scope, but Scope.parse + # is a wrapper over ScopeParser.parse() + from .parser import ScopeParser + + data = ScopeParser.parse(scope_string) if len(data) != 1: raise ValueError( - "Deserializing a scope from string did not get exactly one scope. " + "`Scope.parse()` did not get exactly one scope. " f"Instead got data={data}" ) return data[0] - def serialize(self) -> str: - base_scope = ("*" if self.optional else "") + self.scope_string - if not self.dependencies: - return base_scope - return ( - base_scope + "[" + " ".join(c.serialize() for c in self.dependencies) + "]" - ) - def add_dependency( self, scope: str | Scope, *, optional: bool | None = None ) -> Scope: @@ -140,7 +81,7 @@ def add_dependency( scopeobj = Scope(scope, optional=optional) else: if isinstance(scope, str): - scopeobj = Scope.deserialize(scope) + scopeobj = Scope.parse(scope) else: scopeobj = scope self.dependencies.append(scopeobj) @@ -155,4 +96,7 @@ def __repr__(self) -> str: return "Scope(" + ", ".join(parts) + ")" def __str__(self) -> str: - return self.serialize() + base_scope = ("*" if self.optional else "") + self.scope_string + if not self.dependencies: + return base_scope + return base_scope + "[" + " ".join(str(c) for c in self.dependencies) + "]" diff --git a/tests/benchmark/test_scope_parser.py b/tests/benchmark/test_scope_parser.py index 45313c1a3..fdcc18f63 100644 --- a/tests/benchmark/test_scope_parser.py +++ b/tests/benchmark/test_scope_parser.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk.scopes import Scope +from globus_sdk.scopes import ScopeParser def _make_deep_scope(depth): @@ -23,10 +23,10 @@ def _make_wide_scope(width): @pytest.mark.parametrize("depth", (10, 100, 1000, 2000, 3000, 4000, 5000)) def test_deep_scope_parsing(benchmark, depth): scope_string = _make_deep_scope(depth) - benchmark(Scope.parse, scope_string) + benchmark(ScopeParser.parse, scope_string) @pytest.mark.parametrize("width", (5000, 10000)) def test_wide_scope_parsing(benchmark, width): scope_string = _make_wide_scope(width) - benchmark(Scope.parse, scope_string) + benchmark(ScopeParser.parse, scope_string) diff --git a/tests/common/consents.py b/tests/common/consents.py index a5cb571a0..e05f25bd0 100644 --- a/tests/common/consents.py +++ b/tests/common/consents.py @@ -5,8 +5,8 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta -from globus_sdk import Scope from globus_sdk._types import UUIDLike +from globus_sdk.scopes import Scope, ScopeParser from globus_sdk.scopes.consents import Consent, ConsentForest ScopeRepr = namedtuple("Scope", ["id", "name"]) @@ -79,7 +79,7 @@ def _normalize_scopes(scopes: list[str | Scope] | str | Scope) -> list[Scope]: if isinstance(scopes, Scope): return [scopes] elif isinstance(scopes, str): - return Scope.parse(scopes) + return ScopeParser.parse(scopes) else: to_return = [] for scope in scopes: diff --git a/tests/unit/scopes/test_merge_scopes.py b/tests/unit/scopes/test_merge_scopes.py index 03dad7ce6..1023a0cb3 100644 --- a/tests/unit/scopes/test_merge_scopes.py +++ b/tests/unit/scopes/test_merge_scopes.py @@ -1,13 +1,13 @@ -from globus_sdk.scopes import Scope +from globus_sdk.scopes import Scope, ScopeParser def test_base_scope_strings(): s1 = [Scope("foo"), Scope("bar")] s2 = [Scope("foo"), Scope("baz")] - merged = Scope.merge_scopes(s1, s2) + merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 3 - str_list = [s.serialize() for s in merged] + str_list = [str(s) for s in merged] assert "foo" in str_list assert "bar" in str_list assert "baz" in str_list @@ -16,10 +16,10 @@ def test_base_scope_strings(): def test_mixed_optional_dependencies(): s1 = [Scope("foo", optional=True)] s2 = [Scope("foo", optional=False)] - merged = Scope.merge_scopes(s1, s2) + merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 2 - str_list = [s.serialize() for s in merged] + str_list = [str(s) for s in merged] assert "foo" in str_list assert "*foo" in str_list @@ -27,11 +27,11 @@ def test_mixed_optional_dependencies(): def test_different_dependencies(): s1 = [Scope("foo").add_dependency("bar")] s2 = [Scope("foo").add_dependency("baz")] - merged = Scope.merge_scopes(s1, s2) + merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 1 assert merged[0].scope_string == "foo" - dependency_str_list = [s.serialize() for s in merged[0].dependencies] + dependency_str_list = [str(s) for s in merged[0].dependencies] assert len(dependency_str_list) == 2 assert "bar" in dependency_str_list assert "baz" in dependency_str_list @@ -40,11 +40,11 @@ def test_different_dependencies(): def test_optional_dependencies(): s1 = [Scope("foo").add_dependency("bar")] s2 = [Scope("foo").add_dependency("*bar")] - merged = Scope.merge_scopes(s1, s2) + merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 1 assert merged[0].scope_string == "foo" - dependency_str_list = [s.serialize() for s in merged[0].dependencies] + dependency_str_list = [str(s) for s in merged[0].dependencies] assert len(dependency_str_list) == 1 assert "bar" in dependency_str_list @@ -52,11 +52,11 @@ def test_optional_dependencies(): def test_different_dependencies_on_mixed_optional_base(): s1 = [Scope("foo").add_dependency("bar")] s2 = [Scope("foo", optional=True).add_dependency("baz")] - merged = Scope.merge_scopes(s1, s2) + merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 2 for scope in merged: - dependency_str_list = [s.serialize() for s in scope.dependencies] + dependency_str_list = [str(s) for s in scope.dependencies] assert len(dependency_str_list) == 2 assert "bar" in dependency_str_list assert "baz" in dependency_str_list diff --git a/tests/unit/scopes/test_scope_parser.py b/tests/unit/scopes/test_scope_parser.py index 6cb8b9bcd..c6204d291 100644 --- a/tests/unit/scopes/test_scope_parser.py +++ b/tests/unit/scopes/test_scope_parser.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk import Scope, ScopeCycleError, ScopeParseError +from globus_sdk.scopes import Scope, ScopeCycleError, ScopeParseError, ScopeParser def test_scope_str_and_repr_simple(): @@ -65,7 +65,7 @@ def test_add_dependency_parses_scope_with_optional_marker(): def test_scope_parsing_allows_empty_string(): - scopes = Scope.parse("") + scopes = ScopeParser.parse("") assert scopes == [] @@ -78,8 +78,8 @@ def test_scope_parsing_allows_empty_string(): ], ) def test_scope_parsing_ignores_non_semantic_whitespace(scope_string1, scope_string2): - list1 = Scope.parse(scope_string1) - list2 = Scope.parse(scope_string2) + list1 = ScopeParser.parse(scope_string1) + list2 = ScopeParser.parse(scope_string2) assert len(list1) == len(list2) == 1 s1, s2 = list1[0], list2[0] # Scope.__eq__ is not defined, so equivalence checking is manual (and somewhat error @@ -125,7 +125,7 @@ def test_scope_parsing_ignores_non_semantic_whitespace(scope_string1, scope_stri ) def test_scope_parsing_rejects_bad_inputs(scopestring): with pytest.raises(ScopeParseError): - Scope.parse(scopestring) + ScopeParser.parse(scopestring) @pytest.mark.parametrize( @@ -141,7 +141,7 @@ def test_scope_parsing_rejects_bad_inputs(scopestring): ) def test_scope_parsing_catches_and_rejects_cycles(scopestring): with pytest.raises(ScopeCycleError): - Scope.parse(scopestring) + ScopeParser.parse(scopestring) @pytest.mark.flaky @@ -166,7 +166,7 @@ def test_scope_parsing_catches_and_rejects_very_large_cycles_quickly(): t0 = time.time() with pytest.raises(ScopeCycleError): - Scope.parse(scope_string) + ScopeParser.parse(scope_string) t1 = time.time() assert t1 - t0 < 0.1 @@ -177,31 +177,31 @@ def test_scope_parsing_catches_and_rejects_very_large_cycles_quickly(): ) def test_scope_parsing_accepts_valid_inputs(scopestring): # test *only* that parsing does not error and returns a non-empty list of scopes - scopes = Scope.parse(scopestring) + scopes = ScopeParser.parse(scopestring) assert isinstance(scopes, list) assert len(scopes) > 0 assert isinstance(scopes[0], Scope) def test_scope_deserialize_simple(): - scope = Scope.deserialize("foo") + scope = Scope.parse("foo") assert str(scope) == "foo" def test_scope_deserialize_with_dependencies(): # oh, while we're here, let's also check that our whitespace insensitivity works - scope = Scope.deserialize("foo[ bar *baz ]") + scope = Scope.parse("foo[ bar *baz ]") assert str(scope) in ("foo[bar *baz]", "foo[*baz bar]") def test_scope_deserialize_fails_on_empty(): with pytest.raises(ValueError): - Scope.deserialize(" ") + Scope.parse(" ") def test_scope_deserialize_fails_on_multiple_top_level_scopes(): with pytest.raises(ValueError): - Scope.deserialize("foo bar") + Scope.parse("foo bar") @pytest.mark.parametrize("scope_str", ("*foo", "foo[bar]", "foo[", "foo]", "foo bar")) @@ -220,4 +220,4 @@ def test_scope_init_forbids_special_chars(scope_str): ], ) def test_scope_parsing_normalizes_optionals(original, reserialized): - assert {s.serialize() for s in Scope.parse(original)} == reserialized + assert {str(s) for s in ScopeParser.parse(original)} == reserialized diff --git a/tests/unit/scopes/test_scope_parser_intermediate_representations.py b/tests/unit/scopes/test_scope_parser_intermediate_representations.py index 06a3d89d9..f72da6e38 100644 --- a/tests/unit/scopes/test_scope_parser_intermediate_representations.py +++ b/tests/unit/scopes/test_scope_parser_intermediate_representations.py @@ -1,8 +1,8 @@ -from globus_sdk.scopes._parser import ScopeTreeNode, parse_scope_graph +from globus_sdk.scopes._graph_parser import ScopeGraph, ScopeTreeNode def test_graph_str_single_node(): - g = parse_scope_graph("foo") + g = ScopeGraph.parse("foo") clean_str = _blank_lines_removed(str(g)) assert ( clean_str @@ -15,7 +15,7 @@ def test_graph_str_single_node(): def test_graph_str_single_optional_node(): - g = parse_scope_graph("*foo") + g = ScopeGraph.parse("*foo") clean_str = _blank_lines_removed(str(g)) assert ( clean_str @@ -28,7 +28,7 @@ def test_graph_str_single_optional_node(): def test_graph_str_single_dependency(): - g = parse_scope_graph("foo[bar]") + g = ScopeGraph.parse("foo[bar]") clean_str = _blank_lines_removed(str(g)) assert ( clean_str @@ -42,7 +42,7 @@ def test_graph_str_single_dependency(): def test_graph_str_optional_dependency(): - g = parse_scope_graph("foo[bar[*baz]]") + g = ScopeGraph.parse("foo[bar[*baz]]") clean_str = _blank_lines_removed(str(g)) assert ( clean_str diff --git a/tests/unit/tokenstorage/v2/test_validating_token_storage.py b/tests/unit/tokenstorage/v2/test_validating_token_storage.py index 8882343a0..bdba81328 100644 --- a/tests/unit/tokenstorage/v2/test_validating_token_storage.py +++ b/tests/unit/tokenstorage/v2/test_validating_token_storage.py @@ -130,7 +130,7 @@ def test_validating_token_storage_evaluates_root_scope_requirements( make_token_response, ): adapter = _make_memstorage_with_scope_validator( - consent_client, {"rs1": [Scope.deserialize("scope1")]} + consent_client, {"rs1": [Scope.parse("scope1")]} ) identity_id = str(uuid.uuid4()) valid_token_response = make_token_response( @@ -154,7 +154,7 @@ def test_storage_with_scope_validator_evaluates_dependent_scope_requirements( make_token_response, consent_client ): adapter = _make_memstorage_with_scope_validator( - consent_client, {"rs1": [Scope.deserialize("scope[subscope]")]} + consent_client, {"rs1": [Scope.parse("scope[subscope]")]} ) token_response = make_token_response(scopes={"rs1": "scope"}) adapter.store_token_response(token_response) From a6e7635aaaf0eb715731048bcdd790912889e0f1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 3 Jun 2025 23:40:25 -0500 Subject: [PATCH 061/176] Make Scope objects immutable - Convert the Scope class declaration to a frozen dataclass - Convert the `dependencies` list to a tuple - On Python versions which support it, set `slots=True` on the dataclass - Continue to use a custom repr, as this makes a number of tests simpler - Replace `add_dependency` with `with_dependency`, `with_dependencies`, and `with_optional` --- ...0603_234923_sirosen_split_scope_parser.rst | 17 ++++ src/globus_sdk/scopes/_graph_parser.py | 11 ++- src/globus_sdk/scopes/parser.py | 4 +- src/globus_sdk/scopes/representation.py | 88 +++++++++++-------- src/globus_sdk/services/timers/client.py | 7 +- src/globus_sdk/services/transfer/client.py | 8 +- .../test_client_credentials_authorizer.py | 2 +- tests/unit/globus_app/test_globus_app.py | 6 +- tests/unit/scopes/test_merge_scopes.py | 12 +-- tests/unit/scopes/test_scope_model.py | 39 ++++++++ tests/unit/scopes/test_scope_parser.py | 39 ++------ ...ope_parser_intermediate_representations.py | 14 +-- 12 files changed, 148 insertions(+), 99 deletions(-) create mode 100644 changelog.d/20250603_234923_sirosen_split_scope_parser.rst create mode 100644 tests/unit/scopes/test_scope_model.py diff --git a/changelog.d/20250603_234923_sirosen_split_scope_parser.rst b/changelog.d/20250603_234923_sirosen_split_scope_parser.rst new file mode 100644 index 000000000..4904fd4d6 --- /dev/null +++ b/changelog.d/20250603_234923_sirosen_split_scope_parser.rst @@ -0,0 +1,17 @@ +Changed +~~~~~~~ + +- ``Scope`` objects are now immutable, and their internal ``dependencies`` are + held in a tuple. (:pr:`NUMBER`) + + - The ``add_dependency`` method has been removed, since mutating a ``Scope`` + is no longer possible. + + - A new evolver method, ``Scope.with_dependency`` has been added. It extends + the ``dependencies`` tuple in a new ``Scope`` object. + + - A batch version of ``Scope.with_dependency`` has been added, + ``Scope.with_dependencies``. + + - An evolver for the ``optional`` field of a ``Scope`` is also now available, + as ``Scope.with_optional``. diff --git a/src/globus_sdk/scopes/_graph_parser.py b/src/globus_sdk/scopes/_graph_parser.py index b3873cb03..198d77a0c 100644 --- a/src/globus_sdk/scopes/_graph_parser.py +++ b/src/globus_sdk/scopes/_graph_parser.py @@ -1,6 +1,7 @@ from __future__ import annotations import dataclasses +import sys import typing as t from collections import defaultdict, deque @@ -136,7 +137,15 @@ def _convert_trees(cls, trees: list[ScopeTreeNode]) -> ScopeGraph: return graph -@dataclasses.dataclass(slots=True) +# pass slots=True on 3.10+ +# it's not strictly necessary, but it improves performance +if sys.version_info >= (3, 10): + _add_dataclass_kwargs = {"slots": True} +else: + _add_dataclass_kwargs = {} + + +@dataclasses.dataclass(**_add_dataclass_kwargs) class ScopeTreeNode: # # This is an intermediate representation for scope parsing. diff --git a/src/globus_sdk/scopes/parser.py b/src/globus_sdk/scopes/parser.py index 476b0dae4..90298c71e 100644 --- a/src/globus_sdk/scopes/parser.py +++ b/src/globus_sdk/scopes/parser.py @@ -44,12 +44,12 @@ def parse(cls, scope_string: str) -> list[Scope]: built_scopes: dict[tuple[str, bool], Scope] = {} for name, optionality in list(scope_graph.breadth_first_walk())[::-1]: - dependencies: list[Scope] = [ + dependencies: tuple[Scope, ...] = tuple( # the lookup in built_scopes here is safe because of the # reversed BFS ordering built_scopes[(dep_name, dep_optional)] for _, dep_name, dep_optional in scope_graph.adjacency_matrix[name] - ] + ) built_scopes[(name, optionality)] = Scope( name, optional=optionality, dependencies=dependencies diff --git a/src/globus_sdk/scopes/representation.py b/src/globus_sdk/scopes/representation.py index e89856d9c..cf731741c 100644 --- a/src/globus_sdk/scopes/representation.py +++ b/src/globus_sdk/scopes/representation.py @@ -1,26 +1,41 @@ from __future__ import annotations import dataclasses -import warnings +import sys +import typing as t +# pass slots=True on 3.10+ +# it's not strictly necessary, but it improves performance +if sys.version_info >= (3, 10): + _add_dataclass_kwargs = {"slots": True} +else: + _add_dataclass_kwargs = {} -@dataclasses.dataclass(slots=True) + +@dataclasses.dataclass(frozen=True, repr=False, **_add_dataclass_kwargs) class Scope: """ - A scope object is a representation of a scope which allows modifications to be - made. In particular, it supports handling scope dependencies via - ``add_dependency``. + A scope object is a representation of a scope and its dynamic dependencies + (other scopes). + + A scope also has optionality, also called its "atomically revovocable" setting. + An optional scope can be revoked without revoking consent for other scopes + which were granted at the same time. + + Scopes are immutable, and provide several evolver methods which produce new + Scopes. In particular, ``with_dependency`` and ``with_dependencies`` create + new scopes with added dependencies. - `str(Scope(...))` produces a valid scope string for use in various methods. + ``str(Scope(...))`` produces a valid scope string for use in various methods. :param scope_string: The string which will be used as the basis for this Scope :param optional: The scope may be marked as optional. This means that the scope can - be declined by the user without declining consent for other scopes + be declined by the user without declining consent for other scopes. """ scope_string: str optional: bool = dataclasses.field(default=False) - dependencies: list[Scope] = dataclasses.field(default_factory=list) + dependencies: tuple[Scope, ...] = dataclasses.field(default=()) def __post_init__( self, @@ -54,38 +69,37 @@ def parse(cls, scope_string: str) -> Scope: ) return data[0] - def add_dependency( - self, scope: str | Scope, *, optional: bool | None = None - ) -> Scope: + def with_dependency(self, other_scope: Scope) -> Scope: """ - Add a scope dependency. The dependent scope relationship will be stored in the - Scope and will be evident in its string representation. + Create a new scope with a dependency. + The dependent scope relationship will be stored in the Scope and will + be evident in its string representation. - :param scope: The scope upon which the current scope depends - :param optional: Mark the dependency an optional one. By default it is not. An - optional scope dependency can be declined by the user without declining - consent for the primary scope + :param other_scope: The scope upon which the current scope depends. """ - if optional is not None: - if isinstance(scope, Scope): - raise ValueError( - "cannot use optional=... with a Scope object as the argument to " - "add_dependency" - ) - warnings.warn( - "Passing 'optional' to add_dependency is deprecated. " - "Construct an optional Scope object instead.", - DeprecationWarning, - stacklevel=2, - ) - scopeobj = Scope(scope, optional=optional) - else: - if isinstance(scope, str): - scopeobj = Scope.parse(scope) - else: - scopeobj = scope - self.dependencies.append(scopeobj) - return self + return dataclasses.replace( + self, dependencies=self.dependencies + (other_scope,) + ) + + def with_dependencies(self, other_scopes: t.Iterable[Scope]) -> Scope: + """ + Create a new scope with added dependencies. + The dependent scope relationships will be stored in the Scope and will + be evident in its string representation. + + :param other_scopes: The scopes upon which the current scope depends. + """ + return dataclasses.replace( + self, dependencies=self.dependencies + tuple(other_scopes) + ) + + def with_optional(self, optional: bool) -> Scope: + """ + Create a new scope with a different 'optional' value. + + :param optional: Whether or not the scope is optional. + """ + return dataclasses.replace(self, optional=optional) def __repr__(self) -> str: parts: list[str] = [f"'{self.scope_string}'"] diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index c4923d160..de6f6a8a9 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -87,15 +87,16 @@ def add_app_transfer_data_access_scope( _guards.validators.uuidlike(f"collection_ids[{i}]", c) transfer_scope = Scope(TransferScopes.all) + dependencies: list[Scope] = [] for coll_id in collection_ids_: data_access_scope = Scope( GCSCollectionScopeBuilder(str(coll_id)).data_access, optional=True, ) - transfer_scope.add_dependency(data_access_scope) + dependencies.append(data_access_scope) + transfer_scope = transfer_scope.with_dependencies(dependencies) - timers_scope = Scope(TimersScopes.timer) - timers_scope.add_dependency(transfer_scope) + timers_scope = Scope(TimersScopes.timer, dependencies=(transfer_scope,)) self.add_app_scope(timers_scope) return self diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index f46c3535b..64ab05554 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -198,14 +198,16 @@ def add_app_data_access_scope( for i, c in enumerate(collection_ids_): _guards.validators.uuidlike(f"collection_ids[{i}]", c) - base_scope = Scope(TransferScopes.all) + scope = Scope(TransferScopes.all) + dependencies: list[Scope] = [] for coll_id in collection_ids_: data_access_scope = Scope( GCSCollectionScopeBuilder(str(coll_id)).data_access, optional=True, ) - base_scope.add_dependency(data_access_scope) - self.add_app_scope(base_scope) + dependencies.append(data_access_scope) + scope = scope.with_dependencies(dependencies) + self.add_app_scope(scope) return self # Convenience methods, providing more pythonic access to common REST diff --git a/tests/unit/authorizers/test_client_credentials_authorizer.py b/tests/unit/authorizers/test_client_credentials_authorizer.py index e7cd46227..1ceecb962 100644 --- a/tests/unit/authorizers/test_client_credentials_authorizer.py +++ b/tests/unit/authorizers/test_client_credentials_authorizer.py @@ -67,6 +67,6 @@ def test_can_create_authorizer_from_scope_objects(client): assert a1.scopes == "foo" a2 = ClientCredentialsAuthorizer( - client, [Scope("foo"), "bar", Scope("baz").add_dependency("buzz")] + client, [Scope("foo"), "bar", Scope("baz").with_dependency("buzz")] ) assert a2.scopes == "foo bar baz[buzz]" diff --git a/tests/unit/globus_app/test_globus_app.py b/tests/unit/globus_app/test_globus_app.py index 25cea7cb2..13cd38529 100644 --- a/tests/unit/globus_app/test_globus_app.py +++ b/tests/unit/globus_app/test_globus_app.py @@ -291,7 +291,7 @@ def test_add_scope_requirements_and_auth_params_with_required_scopes(): # adding a requirement with a dependency user_app.add_scope_requirements( - {"foo": [Scope("foo:all").add_dependency(Scope("bar:all"))]} + {"foo": [Scope("foo:all").with_dependency(Scope("bar:all"))]} ) params = user_app._auth_params_with_required_scopes() assert sorted(params.required_scopes) == [ @@ -303,7 +303,7 @@ def test_add_scope_requirements_and_auth_params_with_required_scopes(): # re-adding a requirement with a new dependency, dependencies should be combined user_app.add_scope_requirements( - {"foo": [Scope("foo:all").add_dependency(Scope("baz:all"))]} + {"foo": [Scope("foo:all").with_dependency(Scope("baz:all"))]} ) params = user_app._auth_params_with_required_scopes() # order of dependencies is not guaranteed @@ -345,7 +345,7 @@ def test_constructor_scope_requirements_accepts_different_scope_types(scope_coll def test_scope_requirements_returns_copies_scopes(): user_app = UserApp("test-app", client_id="mock_client_id") - foo_scope = Scope("foo:all").add_dependency(Scope("bar:all")) + foo_scope = Scope("foo:all").with_dependency(Scope("bar:all")) user_app.add_scope_requirements({"foo": [foo_scope]}) real_requirements = user_app._scope_requirements diff --git a/tests/unit/scopes/test_merge_scopes.py b/tests/unit/scopes/test_merge_scopes.py index 1023a0cb3..df4734f68 100644 --- a/tests/unit/scopes/test_merge_scopes.py +++ b/tests/unit/scopes/test_merge_scopes.py @@ -25,8 +25,8 @@ def test_mixed_optional_dependencies(): def test_different_dependencies(): - s1 = [Scope("foo").add_dependency("bar")] - s2 = [Scope("foo").add_dependency("baz")] + s1 = [Scope("foo").with_dependency("bar")] + s2 = [Scope("foo").with_dependency("baz")] merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 1 assert merged[0].scope_string == "foo" @@ -38,8 +38,8 @@ def test_different_dependencies(): def test_optional_dependencies(): - s1 = [Scope("foo").add_dependency("bar")] - s2 = [Scope("foo").add_dependency("*bar")] + s1 = [Scope("foo").with_dependency("bar")] + s2 = [Scope("foo").with_dependency("*bar")] merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 1 assert merged[0].scope_string == "foo" @@ -50,8 +50,8 @@ def test_optional_dependencies(): def test_different_dependencies_on_mixed_optional_base(): - s1 = [Scope("foo").add_dependency("bar")] - s2 = [Scope("foo", optional=True).add_dependency("baz")] + s1 = [Scope("foo").with_dependency("bar")] + s2 = [Scope("foo", optional=True).with_dependency("baz")] merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 2 diff --git a/tests/unit/scopes/test_scope_model.py b/tests/unit/scopes/test_scope_model.py new file mode 100644 index 000000000..8f7e13d61 --- /dev/null +++ b/tests/unit/scopes/test_scope_model.py @@ -0,0 +1,39 @@ +import uuid + +from globus_sdk.scopes import Scope + + +def test_scope_with_dependency_leaves_original_unchanged(): + s1 = Scope(uuid.uuid1().hex) + s2 = Scope("s2") + s3 = s1.with_dependency(s2) + + assert s1.scope_string == s3.scope_string + assert len(s1.dependencies) == 0 + assert len(s3.dependencies) == 1 + + +def test_scope_with_dependencies_leaves_original_unchanged(): + s1 = Scope(uuid.uuid1().hex) + s2 = Scope("s2") + s3 = Scope("s3") + s4 = s1.with_dependencies((s2, s3)) + + assert s1.scope_string == s4.scope_string + assert len(s1.dependencies) == 0 + assert len(s4.dependencies) == 2 + + +def test_scope_with_optional_leaves_original_unchanged(): + s1 = Scope(uuid.uuid1().hex) + s2 = s1.with_optional(True) + s3 = s2.with_optional(False) + + assert s1.scope_string == s2.scope_string == s3.scope_string + assert len(s1.dependencies) == 0 + assert len(s2.dependencies) == 0 + assert len(s3.dependencies) == 0 + + assert not s1.optional + assert s2.optional + assert not s3.optional diff --git a/tests/unit/scopes/test_scope_parser.py b/tests/unit/scopes/test_scope_parser.py index c6204d291..b0f7e2b53 100644 --- a/tests/unit/scopes/test_scope_parser.py +++ b/tests/unit/scopes/test_scope_parser.py @@ -19,49 +19,28 @@ def test_scope_str_and_repr_optional(): def test_scope_str_and_repr_with_dependencies(): s = Scope("top") - s.add_dependency("foo") + s = s.with_dependency(Scope("foo")) assert str(s) == "top[foo]" - s.add_dependency("bar") + s = s.with_dependency(Scope("bar")) assert str(s) == "top[foo bar]" - assert repr(s) == "Scope('top', dependencies=[Scope('foo'), Scope('bar')])" - - -def test_add_dependency_warns_on_optional_but_still_has_good_str_and_repr(): - s = Scope("top") - # this should warn, the use of `optional=...` rather than adding a Scope object - # when optional dependencies are wanted is deprecated - with pytest.warns(DeprecationWarning): - s.add_dependency("foo", optional=True) - - # confirm the str representation and repr for good measure - assert str(s) == "top[*foo]" - assert repr(s) == "Scope('top', dependencies=[Scope('foo', optional=True)])" - - -@pytest.mark.parametrize("optional_arg", (True, False)) -def test_add_dependency_fails_if_optional_is_combined_with_scope(optional_arg): - s = Scope("top") - s2 = Scope("bottom") - with pytest.raises(ValueError): - s.add_dependency(s2, optional=optional_arg) + assert repr(s) == "Scope('top', dependencies=(Scope('foo'), Scope('bar')))" def test_scope_str_nested(): - top = Scope("top") - mid = Scope("mid") bottom = Scope("bottom") - mid.add_dependency(bottom) - top.add_dependency(mid) + mid = Scope("mid", dependencies=(bottom,)) + top = Scope("top", dependencies=(mid,)) assert str(bottom) == "bottom" assert str(mid) == "mid[bottom]" assert str(top) == "top[mid[bottom]]" -def test_add_dependency_parses_scope_with_optional_marker(): +def test_scope_with_optional_dependency_stringifies(): s = Scope("top") - s.add_dependency("*subscope") + s = s.with_dependency(Scope("subscope", optional=True)) assert str(s) == "top[*subscope]" - assert repr(s) == "Scope('top', dependencies=[Scope('subscope', optional=True)])" + subscope_repr = "Scope('subscope', optional=True)" + assert repr(s) == f"Scope('top', dependencies=({subscope_repr},))" def test_scope_parsing_allows_empty_string(): diff --git a/tests/unit/scopes/test_scope_parser_intermediate_representations.py b/tests/unit/scopes/test_scope_parser_intermediate_representations.py index f72da6e38..7db32f6eb 100644 --- a/tests/unit/scopes/test_scope_parser_intermediate_representations.py +++ b/tests/unit/scopes/test_scope_parser_intermediate_representations.py @@ -1,4 +1,4 @@ -from globus_sdk.scopes._graph_parser import ScopeGraph, ScopeTreeNode +from globus_sdk.scopes._graph_parser import ScopeGraph def test_graph_str_single_node(): @@ -56,17 +56,5 @@ def test_graph_str_optional_dependency(): ) -def test_treenode_repr(): - t = ScopeTreeNode("foo", optional=False) - assert repr(t) == "ScopeTreeNode('foo')" - - t = ScopeTreeNode("foo", optional=True) - assert repr(t) == "ScopeTreeNode('foo', optional=True)" - - t = ScopeTreeNode("foo", optional=False) - t.dependencies = [ScopeTreeNode("bar", optional=False)] - assert repr(t) == "ScopeTreeNode('foo', dependencies=[ScopeTreeNode('bar')])" - - def _blank_lines_removed(s: str) -> str: return "\n".join(line for line in s.split("\n") if line.strip() != "") From aaf1f43e65741486993561d73101a682de543573 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 4 Jun 2025 00:30:24 -0500 Subject: [PATCH 062/176] Add sections to upgrading guide about scopes --- docs/upgrading.rst | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 44abbdf8d..480ddef98 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -112,6 +112,57 @@ To control when a submission ID is fetched, use submission_id=submission_id, ) +Scopes are Immutable and Have New Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`Scope ` object in v3 of the SDK could be +updated with in-place modifications. +In v4, these objects are now frozen, and their methods have been altered to +suit their immutability. + +In particular, ``add_dependency`` has been replaced with ``with_dependency``, +which builds and returns a new scope rather than making changes to an existing +value. + +Update ``add_dependency`` usage like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.scopes import Scope + + my_scope = Scope(ROOT_SCOPE_STRING) + my_scope.add_dependency(DEPENCENCY_STRING) + + # globus-sdk v4 + from globus_sdk.scopes import Scope + + my_scope = Scope(ROOT_SCOPE_STRING) + my_scope = my_scope.with_dependency(DEPENCENCY_STRING) + +ScopeParser is now separate from Scope +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Scope parsing has been split from ``Scope`` to a new class, ``ScopeParser``. +Additionally, ``Scope.serialize`` and ``Scope.deserialize`` have been removed, +and ``Scope.parse`` is now a wrapper over ``ScopeParser.parse`` which always +builds and returns one scope. + +Users who need to parse multiple scopes should rely on ``ScopeParser.parse``. +For example, update like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.scopes import Scope + + my_scopes: list[Scope] = Scope.parse(scope_string) + + # globus-sdk v4 + from globus_sdk.scopes import Scope, Scopeparser + + my_scopes: list[Scope] = ScopeParser.parse(scope_string) + Deprecated Timers Aliases Removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 91cc7e32440c40111b07e688184d8892c7ecc4e1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Sun, 8 Jun 2025 22:47:20 -0500 Subject: [PATCH 063/176] Update all usages of 'Scope.add_dependency' --- docs/authorization/scopes_and_consents/scopes.rst | 8 ++++---- docs/examples/guest_collection_creation.rst | 4 +++- .../create_guest_collection_client_owned.py | 6 +++--- .../create_guest_collection_user_owned.py | 6 +++--- src/globus_sdk/services/flows/client.py | 5 ++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/authorization/scopes_and_consents/scopes.rst b/docs/authorization/scopes_and_consents/scopes.rst index 624eb860a..8eee3af7d 100644 --- a/docs/authorization/scopes_and_consents/scopes.rst +++ b/docs/authorization/scopes_and_consents/scopes.rst @@ -148,7 +148,7 @@ constructed by means of ``Scope`` methods thusly: transfer_scope = Scope(TransferScopes.all) data_access_scope = GCSCollectionScopeBuilder(MAPPED_COLLECTION_ID).data_access # add data_access as an optional dependency - transfer_scope.add_dependency(data_access_scope, optional=True) + transfer_scope = transfer_scope.with_dependency(data_access_scope, optional=True) ``Scope``\s can be used in most of the same locations where scope strings can be used, but you can also call ``scope.serialize()`` to get a @@ -167,14 +167,14 @@ strings. All scope objects support this by means of their defined >>> from globus_sdk.scopes import Scope >>> foo = Scope("foo") >>> bar = Scope("bar") - >>> bar.add_dependency("baz") - >>> foo.add_dependency(bar) + >>> bar = bar.with_dependency(Scope("baz")) + >>> foo = foo.with_dependency(bar) >>> print(str(foo)) foo[bar[baz]] >>> print(str(bar)) bar[baz] >>> alpha = Scope("alpha") - >>> alpha.add_dependency("beta", optional=True) + >>> alpha = alpha.with_dependency("beta", optional=True) >>> print(str(alpha)) alpha[*beta] >>> print(repr(alpha)) diff --git a/docs/examples/guest_collection_creation.rst b/docs/examples/guest_collection_creation.rst index 51071be68..173441136 100644 --- a/docs/examples/guest_collection_creation.rst +++ b/docs/examples/guest_collection_creation.rst @@ -40,7 +40,9 @@ policy documents passed to create the user credential. # The scope the client will need, note that primary scope is for the endpoint, # but it has a dependency on the mapped collection's data_access scope scope = scopes.Scope(scopes.GCSEndpointScopeBuilder(endpoint_id).manage_collections) - scope.add_dependency(scopes.GCSCollectionScopeBuilder(mapped_collection_id).data_access) + scope = scope.with_dependency( + scopes.GCSCollectionScopeBuilder(mapped_collection_id).data_access + ) # Build a GCSClient to act as the client by using a ClientCredentialsAuthorizor confidential_client = globus_sdk.ConfidentialAppAuthClient( diff --git a/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_client_owned.py b/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_client_owned.py index 2b0577569..394f7a213 100644 --- a/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_client_owned.py +++ b/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_client_owned.py @@ -42,10 +42,10 @@ def attach_data_access_scope(gcs_client, collection_id): endpoint_scopes = gcs_client.get_gcs_endpoint_scopes(gcs_client.endpoint_client_id) collection_scopes = gcs_client.get_gcs_collection_scopes(collection_id) - manage_collections = globus_sdk.Scope(endpoint_scopes.manage_collections) data_access = globus_sdk.Scope(collection_scopes.data_access, optional=True) - - manage_collections.add_dependency(data_access) + manage_collections = globus_sdk.Scope( + endpoint_scopes.manage_collections, dependencies=(data_access,) + ) gcs_client.add_app_scope(manage_collections) diff --git a/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_user_owned.py b/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_user_owned.py index 2401d4fba..784c612b0 100644 --- a/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_user_owned.py +++ b/docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_user_owned.py @@ -36,10 +36,10 @@ def attach_data_access_scope(gcs_client, collection_id): endpoint_scopes = gcs_client.get_gcs_endpoint_scopes(gcs_client.endpoint_client_id) collection_scopes = gcs_client.get_gcs_collection_scopes(collection_id) - manage_collections = globus_sdk.Scope(endpoint_scopes.manage_collections) data_access = globus_sdk.Scope(collection_scopes.data_access, optional=True) - - manage_collections.add_dependency(data_access) + manage_collections = globus_sdk.Scope( + endpoint_scopes.manage_collections, dependencies=(data_access,) + ) gcs_client.add_app_scope(manage_collections) diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 0bdae806f..803b22cfa 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -1010,10 +1010,9 @@ def add_app_transfer_data_access_scope( GCSCollectionScopeBuilder(str(coll_id)).data_access, optional=True, ) - transfer_scope.add_dependency(data_access_scope) + transfer_scope = transfer_scope.with_dependency(data_access_scope) - specific_flow_scope = Scope(self.scopes.user) - specific_flow_scope.add_dependency(transfer_scope) + specific_flow_scope = Scope(self.scopes.user, dependencies=(transfer_scope,)) self.add_app_scope(specific_flow_scope) return self From 2da00462f88193c2c04ac2836a2a2f05621b1591 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 9 Jun 2025 02:57:21 -0500 Subject: [PATCH 064/176] Fix an unannotated dict type under py3.8 When `mypy-py3.8` runs, this dict has no infer-able type parameters, so mypy fails. Simply annotate to avoid the issue for now. --- src/globus_sdk/scopes/_graph_parser.py | 4 ++-- src/globus_sdk/scopes/representation.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/globus_sdk/scopes/_graph_parser.py b/src/globus_sdk/scopes/_graph_parser.py index 198d77a0c..817e08571 100644 --- a/src/globus_sdk/scopes/_graph_parser.py +++ b/src/globus_sdk/scopes/_graph_parser.py @@ -140,9 +140,9 @@ def _convert_trees(cls, trees: list[ScopeTreeNode]) -> ScopeGraph: # pass slots=True on 3.10+ # it's not strictly necessary, but it improves performance if sys.version_info >= (3, 10): - _add_dataclass_kwargs = {"slots": True} + _add_dataclass_kwargs: dict[str, bool] = {"slots": True} else: - _add_dataclass_kwargs = {} + _add_dataclass_kwargs: dict[str, bool] = {} @dataclasses.dataclass(**_add_dataclass_kwargs) diff --git a/src/globus_sdk/scopes/representation.py b/src/globus_sdk/scopes/representation.py index cf731741c..e553c0da8 100644 --- a/src/globus_sdk/scopes/representation.py +++ b/src/globus_sdk/scopes/representation.py @@ -7,9 +7,9 @@ # pass slots=True on 3.10+ # it's not strictly necessary, but it improves performance if sys.version_info >= (3, 10): - _add_dataclass_kwargs = {"slots": True} + _add_dataclass_kwargs: dict[str, bool] = {"slots": True} else: - _add_dataclass_kwargs = {} + _add_dataclass_kwargs: dict[str, bool] = {} @dataclasses.dataclass(frozen=True, repr=False, **_add_dataclass_kwargs) From e64b29f0f80d5487ff0107d34b7f41071c0df607 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 9 Jun 2025 09:45:16 -0500 Subject: [PATCH 065/176] Refine changelog for scope parsing + immutability Merge the files, ensure that immutability comes first (reads better that way), and tune up the phrasing. Also apply PR number substitutions. --- ...0603_181734_sirosen_split_scope_parser.rst | 26 ++++++++++++++++--- ...0603_234923_sirosen_split_scope_parser.rst | 17 ------------ 2 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 changelog.d/20250603_234923_sirosen_split_scope_parser.rst diff --git a/changelog.d/20250603_181734_sirosen_split_scope_parser.rst b/changelog.d/20250603_181734_sirosen_split_scope_parser.rst index e08042148..5b3032cbd 100644 --- a/changelog.d/20250603_181734_sirosen_split_scope_parser.rst +++ b/changelog.d/20250603_181734_sirosen_split_scope_parser.rst @@ -1,17 +1,35 @@ Changed -~~~~~~~ +------- + +- ``Scope`` objects are now immutable. (:pr:`1208`) + + - ``Scope.dependencies`` is now a tuple, not a list. + + - The ``add_dependency`` method has been removed, since mutating a ``Scope`` + is no longer possible. + + - A new evolver method, ``Scope.with_dependency`` has been added. It extends + the ``dependencies`` tuple in a new ``Scope`` object. + + - A batch version of ``Scope.with_dependency`` has been added, + ``Scope.with_dependencies``. + + - An evolver for the ``optional`` field of a ``Scope`` is also now available, + named ``Scope.with_optional``. - Scope parsing has been separated from the main ``Scope`` class into a - dedicated ``ScopeParser`` which provides parsing methods. (:pr:`NUMBER`) + dedicated ``ScopeParser`` which provides parsing methods. (:pr:`1208`) - Use ``globus_sdk.scopes.ScopeParser`` for complex parsing use-cases. The ``ScopeParser.parse`` classmethod parses strings into lists of scope objects. - - ``Scope.serialize`` and ``Scope.deserialize`` have been removed as methods. + - ``Scope.merge_scopes`` has been moved to ``ScopeParser.merge_scopes``. - ``Scope.parse`` is changed to call ``ScopeParser.parse`` and verify that there is exactly one result, which it returns. This means that ``Scope.parse`` now returns a single ``Scope``, not a ``list[Scope]``. - - ``Scope.merge_scopes`` has been moved to ``ScopeParser.merge_scopes``. + - ``Scope.serialize`` and ``Scope.deserialize`` have been removed as methods. + Use ``str(scope_object)`` as a replacement for ``serialize()`` and + ``Scope.parse`` as a replacement for ``deserialize()``. diff --git a/changelog.d/20250603_234923_sirosen_split_scope_parser.rst b/changelog.d/20250603_234923_sirosen_split_scope_parser.rst deleted file mode 100644 index 4904fd4d6..000000000 --- a/changelog.d/20250603_234923_sirosen_split_scope_parser.rst +++ /dev/null @@ -1,17 +0,0 @@ -Changed -~~~~~~~ - -- ``Scope`` objects are now immutable, and their internal ``dependencies`` are - held in a tuple. (:pr:`NUMBER`) - - - The ``add_dependency`` method has been removed, since mutating a ``Scope`` - is no longer possible. - - - A new evolver method, ``Scope.with_dependency`` has been added. It extends - the ``dependencies`` tuple in a new ``Scope`` object. - - - A batch version of ``Scope.with_dependency`` has been added, - ``Scope.with_dependencies``. - - - An evolver for the ``optional`` field of a ``Scope`` is also now available, - as ``Scope.with_optional``. From d412ab9bd5e8e5d2f1809f1248da986821efad70 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 23 Jun 2025 15:12:39 -0500 Subject: [PATCH 066/176] Apply suggestions from code review Co-authored-by: Ada <107940310+ada-globus@users.noreply.github.com> --- docs/upgrading.rst | 4 ++-- src/globus_sdk/scopes/_graph_parser.py | 5 +++-- src/globus_sdk/scopes/representation.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 480ddef98..7deccc133 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -112,7 +112,7 @@ To control when a submission ID is fetched, use submission_id=submission_id, ) -Scopes are Immutable and Have New Methods +Scopes Are Immutable and Have New Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :class:`Scope ` object in v3 of the SDK could be @@ -140,7 +140,7 @@ Update ``add_dependency`` usage like so: my_scope = Scope(ROOT_SCOPE_STRING) my_scope = my_scope.with_dependency(DEPENCENCY_STRING) -ScopeParser is now separate from Scope +ScopeParser Is Now Separate from Scope ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Scope parsing has been split from ``Scope`` to a new class, ``ScopeParser``. diff --git a/src/globus_sdk/scopes/_graph_parser.py b/src/globus_sdk/scopes/_graph_parser.py index 817e08571..86bf06e57 100644 --- a/src/globus_sdk/scopes/_graph_parser.py +++ b/src/globus_sdk/scopes/_graph_parser.py @@ -43,8 +43,9 @@ def _normalize_optionals(self) -> None: src, dest, optional = edge if not optional: continue - alter_ego = (src, dest, not optional) - if alter_ego in self.edges: + # The current edge is optional; see if it's superseded by required edge + required_variant = (src, dest, False) + if required_variant in self.edges: to_remove.add(edge) self.edges = self.edges - to_remove for edge in to_remove: diff --git a/src/globus_sdk/scopes/representation.py b/src/globus_sdk/scopes/representation.py index e553c0da8..15c4697d8 100644 --- a/src/globus_sdk/scopes/representation.py +++ b/src/globus_sdk/scopes/representation.py @@ -18,7 +18,7 @@ class Scope: A scope object is a representation of a scope and its dynamic dependencies (other scopes). - A scope also has optionality, also called its "atomically revovocable" setting. + A scope may be optional (also referred to as "atomically revocable"). An optional scope can be revoked without revoking consent for other scopes which were granted at the same time. From 0f540ec99e5ab955e648bfd268d6e1788636f12c Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 24 Jun 2025 10:15:36 -0500 Subject: [PATCH 067/176] Add minor comment about type-ignore usage --- src/globus_sdk/login_flows/login_flow_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/globus_sdk/login_flows/login_flow_manager.py b/src/globus_sdk/login_flows/login_flow_manager.py index 548b96256..09e791a01 100644 --- a/src/globus_sdk/login_flows/login_flow_manager.py +++ b/src/globus_sdk/login_flows/login_flow_manager.py @@ -52,6 +52,8 @@ def _get_authorize_url( """ self._oauth2_start_flow(auth_parameters, redirect_uri) + # prompt is assigned first because its usage is type-ignored below + # this makes it clear that the ignore applies to that usage prompt = none2missing(auth_parameters.prompt) return self.login_client.oauth2_get_authorize_url( session_required_identities=none2missing( From 372533e273ef84dadb8890eecc54cc9d02b35853 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 24 Jun 2025 11:13:23 -0500 Subject: [PATCH 068/176] Fix a bug recently introduced into a typing test As part of the scope parsing changes, `Scope.parse()` changed in type. CI did not capture this failure for some reason. --- tests/non-pytest/mypy-ignore-tests/test_consents_usage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/non-pytest/mypy-ignore-tests/test_consents_usage.py b/tests/non-pytest/mypy-ignore-tests/test_consents_usage.py index 177eeccf4..46429dd9c 100644 --- a/tests/non-pytest/mypy-ignore-tests/test_consents_usage.py +++ b/tests/non-pytest/mypy-ignore-tests/test_consents_usage.py @@ -1,6 +1,7 @@ import uuid from globus_sdk import AuthClient, Scope +from globus_sdk.scopes import ScopeParser # setup: get a consent forest ac = AuthClient() @@ -11,7 +12,8 @@ # create some variant types xfer_str: str = "urn:globus:auth:scope:transfer.api.globus.org:all" strlist: list[str] = [xfer_str] -scopelist: list[Scope] = Scope.parse(xfer_str) +scopelist: list[Scope] = ScopeParser.parse(xfer_str) +scopeobj: Scope = Scope.parse(xfer_str) # all should be allowed b: bool From 63ff466a8e385c1196a6ca957b61228a2042e29c Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 24 Jun 2025 15:00:03 -0500 Subject: [PATCH 069/176] Remove '.mypy_cache' from GHA caching This is an ideally temporary fix for a bad cache scenario we have uncovered. --- .github/workflows/test.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1dba99bc4..f8d054e57 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -80,8 +80,12 @@ jobs: - "pylint" - "test-lazy-imports" - "twine-check" - cache-paths: - - ".mypy_cache/" + # TODO: revisit caching strategy for '.mypy_cache' + # we believe a bad cache can be restored when the package changes, + # tricking 'mypy-test' into seeing an old version of the SDK + # + # cache-paths: + # - ".mypy_cache/" uses: "globus/workflows/.github/workflows/tox.yaml@f41714f6a8b102569807b348fce50960f9617df8" # v1.2 with: From 0427f5ab3469cbaf23e71a09c99980889db7f787 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 23 Jun 2025 16:19:03 -0500 Subject: [PATCH 070/176] Replace ScopeBuilder types with Scope Collections ScopeBuilders supported the construction of instances with dynamic attributes which resolved to scope strings. Scope collections represent a bit of a data rearchitecutre: 1. Scope Collections are split into Static and Dynamic classes, so that the "per agent/instance" and "per service" cases can be described as distinct. 2. None of the attributes are dynamic; everything is available to static analysis, primarily as class attributes. 3. Scopes are represented as `Scope` objects, not strings. Some particular notes: - `StaticScopeCollectionMeta` is defined so that `str(AuthClient.scopes)` and similar access paths still provide a good experience which enumerates the available scopes. - `ComputeScopes`, `FlowsScopes`, and `AuthScopes` are all simplified now that the class in use is actually simpler and requires fewer workarounds. --- ...1204_sirosen_service_scopes_collection.rst | 13 ++ .../scopes_and_consents/scopes.rst | 23 ++-- .../directives/list_known_scopes.py | 32 +++-- src/globus_sdk/client.py | 6 +- src/globus_sdk/globus_app/app.py | 2 +- src/globus_sdk/scopes/__init__.py | 17 +-- src/globus_sdk/scopes/builder.py | 130 ------------------ src/globus_sdk/scopes/collection.py | 105 ++++++++++++++ src/globus_sdk/scopes/consents/_model.py | 10 +- src/globus_sdk/scopes/data/__init__.py | 10 +- src/globus_sdk/scopes/data/auth.py | 33 ++--- src/globus_sdk/scopes/data/compute.py | 40 +----- src/globus_sdk/scopes/data/flows.py | 91 +++++------- src/globus_sdk/scopes/data/gcs.py | 47 ++++--- src/globus_sdk/scopes/data/groups.py | 27 ++-- src/globus_sdk/scopes/data/search.py | 19 ++- src/globus_sdk/scopes/data/timers.py | 13 +- src/globus_sdk/scopes/data/transfer.py | 15 +- .../services/auth/client/service_client.py | 6 +- src/globus_sdk/services/compute/client.py | 6 +- src/globus_sdk/services/flows/client.py | 26 ++-- src/globus_sdk/services/gcs/client.py | 24 ++-- src/globus_sdk/services/groups/client.py | 2 +- src/globus_sdk/services/search/client.py | 7 +- src/globus_sdk/services/timers/client.py | 16 +-- src/globus_sdk/services/transfer/client.py | 13 +- .../services/auth/test_auth_client_flow.py | 4 +- .../services/gcs/test_scope_helpers.py | 29 ++-- .../mypy-ignore-tests/specific_flow_scopes.py | 2 +- .../globus_app/test_client_integration.py | 6 +- tests/unit/scopes/test_scope_builder.py | 107 -------------- tests/unit/scopes/test_scope_collections.py | 90 ++++++++++++ .../unit/sphinxext/test_list_known_scopes.py | 8 +- tests/unit/test_base_client.py | 10 +- tests/unit/test_specific_flows_client.py | 9 +- 35 files changed, 460 insertions(+), 538 deletions(-) create mode 100644 changelog.d/20250624_011204_sirosen_service_scopes_collection.rst delete mode 100644 src/globus_sdk/scopes/builder.py create mode 100644 src/globus_sdk/scopes/collection.py delete mode 100644 tests/unit/scopes/test_scope_builder.py create mode 100644 tests/unit/scopes/test_scope_collections.py diff --git a/changelog.d/20250624_011204_sirosen_service_scopes_collection.rst b/changelog.d/20250624_011204_sirosen_service_scopes_collection.rst new file mode 100644 index 000000000..d8d65c5bc --- /dev/null +++ b/changelog.d/20250624_011204_sirosen_service_scopes_collection.rst @@ -0,0 +1,13 @@ +Changed +------- + +- The SDK's ``ScopeBuilder`` types have been replaced with + ``StaticScopeCollection`` and ``DynamicScopeCollection`` types. (:pr:`NUMBER`) + + - Scopes provided as constants by the SDK are now ``Scope`` objects, not + strings. They can be converted to strings trivially with ``str(scope)``. + + - The various scope builder types have been renamed. ``SpecificFlowScopes``, + ``GCSEndpointScopes``, and ``GCSCollectionScopes`` replace + ``SpecificFlowScopeBuilder``, ``GCSEndpointScopeBuilder``, and + ``GCSCollectionScopeBuilder``. diff --git a/docs/authorization/scopes_and_consents/scopes.rst b/docs/authorization/scopes_and_consents/scopes.rst index 8eee3af7d..cafd1b724 100644 --- a/docs/authorization/scopes_and_consents/scopes.rst +++ b/docs/authorization/scopes_and_consents/scopes.rst @@ -145,10 +145,9 @@ constructed by means of ``Scope`` methods thusly: MAPPED_COLLECTION_ID = "...ID HERE..." # create the scope object, and get the data_access_scope as a string - transfer_scope = Scope(TransferScopes.all) data_access_scope = GCSCollectionScopeBuilder(MAPPED_COLLECTION_ID).data_access # add data_access as an optional dependency - transfer_scope = transfer_scope.with_dependency(data_access_scope, optional=True) + transfer_scope = TransferScopes.all.with_dependency(data_access_scope, optional=True) ``Scope``\s can be used in most of the same locations where scope strings can be used, but you can also call ``scope.serialize()`` to get a @@ -203,27 +202,31 @@ manipulate scope objects. ScopeBuilders ------------- -ScopeBuilder Types -~~~~~~~~~~~~~~~~~~ +Scope Collection Types +~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: ScopeBuilder +.. autoclass:: StaticScopeCollection :members: :show-inheritance: -.. autoclass:: GCSEndpointScopeBuilder +.. autoclass:: DynamicScopeCollection :members: :show-inheritance: -.. autoclass:: GCSCollectionScopeBuilder +.. autoclass:: GCSEndpointScopes :members: :show-inheritance: -.. autoclass:: SpecificFlowScopeBuilder +.. autoclass:: GCSCollectionScopes :members: :show-inheritance: -ScopeBuilder Constants -~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: SpecificFlowScopes + :members: + :show-inheritance: + +ScopeCollection Constants +~~~~~~~~~~~~~~~~~~~~~~~~~ .. py:data:: globus_sdk.scopes.data.AuthScopes diff --git a/src/globus_sdk/_sphinxext/directives/list_known_scopes.py b/src/globus_sdk/_sphinxext/directives/list_known_scopes.py index 95d9c5fe2..33bec88c8 100644 --- a/src/globus_sdk/_sphinxext/directives/list_known_scopes.py +++ b/src/globus_sdk/_sphinxext/directives/list_known_scopes.py @@ -5,7 +5,7 @@ from docutils.parsers.rst import directives -from globus_sdk.scopes import ScopeBuilder +from globus_sdk.scopes import DynamicScopeCollection, StaticScopeCollection from .add_content_directive import AddContentDirective @@ -16,20 +16,21 @@ class ListKnownScopes(AddContentDirective): optional_arguments = 0 option_spec = { "example_scope": directives.unchanged, - # Allow overriding the base name to match how the ScopeBuilder will be accessed. + # Allow overriding the base name to match how the ScopeCollection will + # be accessed. "base_name": directives.unchanged, } def gen_rst(self) -> t.Iterator[str]: - sb_name = self.arguments[0] - sb_basename = sb_name.split(".")[-1] + sc_name = self.arguments[0] + sc_basename = sc_name.split(".")[-1] if "base_name" in self.options: - sb_basename = self.options["base_name"] + sc_basename = self.options["base_name"] example_scope = None if "example_scope" in self.options: example_scope = self.options["example_scope"].strip() - known_scopes = extract_known_scopes(sb_name) + known_scopes = extract_known_scopes(sc_name) if example_scope is None: example_scope = known_scopes[0] @@ -37,7 +38,7 @@ def gen_rst(self) -> t.Iterator[str]: yield "Various scopes are available as attributes of this object." yield f"For example, access the ``{example_scope}`` scope with" yield "" - yield f">>> {sb_basename}.{example_scope}" + yield f">>> {sc_basename}.{example_scope}" yield "" yield "**Supported Scopes**" yield "" @@ -47,10 +48,13 @@ def gen_rst(self) -> t.Iterator[str]: yield "" -def extract_known_scopes(scope_builder_name: str) -> list[str]: - sb = locate(scope_builder_name) - if not isinstance(sb, ScopeBuilder): - raise RuntimeError( - f"Expected {sb} to be a ScopeBuilder, but got {type(sb)} instead" - ) - return sb.scope_names +def extract_known_scopes(scope_collection_name: str) -> list[str]: + sc = locate(scope_collection_name) + if isinstance(sc, DynamicScopeCollection): + return list(sc._scope_names) + elif isinstance(sc, type) and issubclass(sc, StaticScopeCollection): + return list(sc._scope_names()) + + raise RuntimeError( + f"Expected {sc} to be a scope collection, but got {type(sc)} instead" + ) diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index ee937d67c..c802ff1ac 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -12,7 +12,7 @@ from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.paging import PaginatorTable from globus_sdk.response import GlobusHTTPResponse -from globus_sdk.scopes import Scope, ScopeBuilder +from globus_sdk.scopes import DynamicScopeCollection, Scope, StaticScopeCollection from globus_sdk.transport import RequestsTransport if sys.version_info >= (3, 10): @@ -69,8 +69,8 @@ class BaseClient: #: 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 ``ScopeBuilder`` - scopes: ScopeBuilder | None = None + #: the scopes for this client may be present as a ``ScopeCollection`` + scopes: type[StaticScopeCollection] | DynamicScopeCollection | None = None def __init__( self, diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index 674c63e47..c66662ff6 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -116,7 +116,7 @@ def __init__( # # additionally, this will ensure that openid scope requirement is always # registered (it's required for token identity validation). - consent_client.attach_globus_app(self, app_scopes=[Scope(AuthScopes.openid)]) + consent_client.attach_globus_app(self, app_scopes=[AuthScopes.openid]) def _resolve_scope_requirements( self, scope_requirements: t.Mapping[str, ScopeCollectionType] | None diff --git a/src/globus_sdk/scopes/__init__.py b/src/globus_sdk/scopes/__init__.py index 93c53b24e..f6fb960ec 100644 --- a/src/globus_sdk/scopes/__init__.py +++ b/src/globus_sdk/scopes/__init__.py @@ -1,15 +1,15 @@ from ._normalize import scopes_to_scope_list, scopes_to_str -from .builder import ScopeBuilder +from .collection import DynamicScopeCollection, StaticScopeCollection from .data import ( AuthScopes, ComputeScopes, FlowsScopes, - GCSCollectionScopeBuilder, - GCSEndpointScopeBuilder, + GCSCollectionScopes, + GCSEndpointScopes, GroupsScopes, NexusScopes, SearchScopes, - SpecificFlowScopeBuilder, + SpecificFlowScopes, TimersScopes, TransferScopes, ) @@ -18,17 +18,18 @@ from .representation import Scope __all__ = ( - "ScopeBuilder", + "StaticScopeCollection", + "DynamicScopeCollection", "Scope", "ScopeParser", "ScopeParseError", "ScopeCycleError", - "GCSCollectionScopeBuilder", - "GCSEndpointScopeBuilder", + "GCSCollectionScopes", + "GCSEndpointScopes", "AuthScopes", "ComputeScopes", "FlowsScopes", - "SpecificFlowScopeBuilder", + "SpecificFlowScopes", "GroupsScopes", "NexusScopes", "SearchScopes", diff --git a/src/globus_sdk/scopes/builder.py b/src/globus_sdk/scopes/builder.py deleted file mode 100644 index a645eaa4c..000000000 --- a/src/globus_sdk/scopes/builder.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import annotations - -import typing as t - -ScopeBuilderScopes = t.Union[ - None, - str, - t.Tuple[str, str], - t.List[t.Union[str, t.Tuple[str, str]]], -] - - -class ScopeBuilder: - """ - Utility class for creating scope strings for a specified resource server. - - :param resource_server: The identifier, usually a domain name or a UUID, for the - resource server to return scopes for. - :param known_scopes: A list of scope names to pre-populate on this instance. This - will set attributes on the instance using the URN scope format. - :param known_url_scopes: A list of scope names to pre-populate on this instance. - This will set attributes on the instance using the URL scope format. - """ - - _classattr_scope_names: list[str] = [] - - def __init__( - self, - resource_server: str, - *, - known_scopes: ScopeBuilderScopes = None, - known_url_scopes: ScopeBuilderScopes = None, - ) -> None: - self.resource_server = resource_server - - self._registered_scope_names: list[str] = [] - self._register_scopes(known_scopes, self.urn_scope_string) - self._register_scopes(known_url_scopes, self.url_scope_string) - - def _register_scopes( - self, scopes: ScopeBuilderScopes, transform_func: t.Callable[[str], str] - ) -> None: - scopes_dict = self._scopes_input_to_dict(scopes) - for scope_name, scope_val in scopes_dict.items(): - self._registered_scope_names.append(scope_name) - setattr(self, scope_name, transform_func(scope_val)) - - def _scopes_input_to_dict(self, items: ScopeBuilderScopes) -> dict[str, str]: - """ - ScopeBuilders accepts many collection-style types of scopes. This function - normalizes all of those types into a standard {scope_name: scope_val} dict - - Translation Map: - None => {} - "my-str" => {"my-str": "my-str"} - ["my-list"] => {"my-list": "my-list"} - ("my-tuple-key", "my-tuple-val") => {"my-tuple-key": "my-tuple-val"} - """ - if items is None: - return {} - elif isinstance(items, str): - return {items: items} - elif isinstance(items, tuple): - return {items[0]: items[1]} - else: - items_dict = {} - for item in items: - if isinstance(item, str): - items_dict[item] = item - else: - items_dict[item[0]] = item[1] - return items_dict - - @property - def scope_names(self) -> list[str]: - return self._classattr_scope_names + self._registered_scope_names - - # custom __getattr__ instructs `mypy` that unknown attributes of a ScopeBuilder are - # of type `str`, allowing for dynamic attribute names - # to test, try creating a module with - # - # from globus_sdk.scopes import TransferScopes - # x = TransferScopes.all - # - # without this method, the assignment to `x` would fail type checking - # because `all` is unknown to mypy - # - # note that the implementation just raises AttributeError; this is okay because - # __getattr__ is only called as a last resort, when __getattribute__ has failed - # normal attribute access will not be disrupted - def __getattr__(self, name: str) -> str: - raise AttributeError(f"Unrecognized Attribute '{name}'") - - def urn_scope_string(self, scope_name: str) -> str: - """ - Return a complete string representing the scope with a given name for this - client, in the Globus Auth URN format. - - Note that this module already provides many such scope strings for use with - Globus services. - - **Examples** - - >>> sb = ScopeBuilder("transfer.api.globus.org") - >>> sb.urn_scope_string("transfer.api.globus.org", "all") - "urn:globus:auth:scope:transfer.api.globus.org:all" - - :param scope_name: The short name for the scope involved. - """ - return f"urn:globus:auth:scope:{self.resource_server}:{scope_name}" - - def url_scope_string(self, scope_name: str) -> str: - """ - Return a complete string representing the scope with a given name for this - client, in URL format. - - **Examples** - - >>> sb = ScopeBuilder("actions.globus.org") - >>> sb.url_scope_string("actions.globus.org", "hello_world") - "https://auth.globus.org/scopes/actions.globus.org/hello_world" - - :param scope_name: The short name for the scope involved. - """ - return f"https://auth.globus.org/scopes/{self.resource_server}/{scope_name}" - - def __str__(self) -> str: - return f"{self.__class__.__name__}[{self.resource_server}]\n" + "\n".join( - f" {name}:\n {getattr(self, name)}" for name in self.scope_names - ) diff --git a/src/globus_sdk/scopes/collection.py b/src/globus_sdk/scopes/collection.py new file mode 100644 index 000000000..20fa55e5c --- /dev/null +++ b/src/globus_sdk/scopes/collection.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import typing as t + +from .representation import Scope + + +class StaticScopeCollectionMeta(type): + """ + The metaclass for StaticScopeCollection. + + This defines the the stringification of these classes. + """ + + def __str__(self) -> str: + name: str = getattr(self, "__name__", "") + resource_server: str = getattr( + self, "resource_server", "" + ) + + scope_names: t.Iterable[str] = self._scope_names() # type: ignore[attr-defined] + + return f"{name}[{resource_server}]\n" + "\n".join( + f" {name}:\n {getattr(self, name)}" for name in scope_names + ) + + +class StaticScopeCollection(metaclass=StaticScopeCollectionMeta): + """ + A static scope collection is a data container which provides various scopes + as class attributes. + + ``resource_server`` is available as a class attribute. + + ``str()`` is well-defined to produce a nice rendering of the scopes + contained in the class. + """ + + resource_server: t.ClassVar[str] + + @classmethod + def _scope_names(cls) -> t.Iterator[str]: + for key, value in vars(cls).items(): + if isinstance(value, Scope): + yield key + + +class DynamicScopeCollection: + """ + The base type for dynamic scope collections, where the resource server is + variable. + + The class itself is not usable as a collection type, but its instances are. + + The default implementation takes the resource server as the only init-time + parameter. + + :param resource_server: The resource_server to use for all scopes attached + to this scope collection. + """ + + # DynamicScopeCollection classes are expected to provide + # the scope names which they provide as a classvar + # these are often properties for dynamic computation + _scope_names: t.ClassVar[tuple[str, ...]] + + def __init__(self, resource_server: str) -> None: + self.resource_server = resource_server + + def __str__(self) -> str: + return f"{self.__class__.__name__}[{self.resource_server}]\n" + "\n".join( + f" {name}:\n {getattr(self, name)}" for name in self._scope_names + ) + + +def _urn_scope(resource_server: str, scope_name: str) -> Scope: + """ + Convert a short name + resource server string to a scope, in the Globus + Auth URN format. + + Example Usage: + + >>> _urn_scope("transfer.api.globus.org", "all") + Scope('urn:globus:auth:scope:transfer.api.globus.org:all') + + :param resource_server: The resource server string. + :param scope_name: The short name for the scope. + """ + return Scope(f"urn:globus:auth:scope:{resource_server}:{scope_name}") + + +def _url_scope(resource_server: str, scope_name: str) -> Scope: + """ + Convert a short name + resource server string to a scope, in the Globus + Auth URL format. + + Example Usage: + + >>> _url_scope("actions.globus.org", "hello_world") + Scope('https://auth.globus.org/scopes/actions.globus.org/hello_world') + + :param resource_server: The resource server string. + :param scope_name: The short name for the scope. + """ + return Scope(f"https://auth.globus.org/scopes/{resource_server}/{scope_name}") diff --git a/src/globus_sdk/scopes/consents/_model.py b/src/globus_sdk/scopes/consents/_model.py index 4c64a25a5..a5009bf6f 100644 --- a/src/globus_sdk/scopes/consents/_model.py +++ b/src/globus_sdk/scopes/consents/_model.py @@ -109,7 +109,7 @@ class ConsentForest: It exists to expose a simple interface for evaluating whether resource server grant requirements, as defined by a scope object are satisfied. - Consents should be retrieved from the AuthClient's `get_consents` method. + Consents should be retrieved from the AuthClient's ``get_consents`` method. Example usage: @@ -117,10 +117,10 @@ class ConsentForest: >>> identity_id = ... >>> forest = auth_client.get_consents(identity_id).to_forest() >>> - >>> # Check whether the forest contains a scope relationship - >>> dependent_scope = GCSCollectionScopeBuilder(collection_id).data_access - >>> scope = f"{TransferScopes.all}[{dependent_scope}]" - >>> forest.contains_scopes(scope) + >>> # Check whether the forest meets a scope requirement + >>> data_access_scope = GCSCollectionScopes(collection_id).data_access + >>> scope = TransferScopes.all.with_dependency(data_access_scope) + >>> forest.meets_scope_requirements(scope) The following diagram demonstrates a Consent Forest in which a user has consented diff --git a/src/globus_sdk/scopes/data/__init__.py b/src/globus_sdk/scopes/data/__init__.py index 35b9ab164..94caff0b3 100644 --- a/src/globus_sdk/scopes/data/__init__.py +++ b/src/globus_sdk/scopes/data/__init__.py @@ -1,7 +1,7 @@ from .auth import AuthScopes from .compute import ComputeScopes -from .flows import FlowsScopes, SpecificFlowScopeBuilder -from .gcs import GCSCollectionScopeBuilder, GCSEndpointScopeBuilder +from .flows import FlowsScopes, SpecificFlowScopes +from .gcs import GCSCollectionScopes, GCSEndpointScopes from .groups import GroupsScopes, NexusScopes from .search import SearchScopes from .timers import TimersScopes @@ -11,9 +11,9 @@ "AuthScopes", "ComputeScopes", "FlowsScopes", - "SpecificFlowScopeBuilder", - "GCSEndpointScopeBuilder", - "GCSCollectionScopeBuilder", + "SpecificFlowScopes", + "GCSEndpointScopes", + "GCSCollectionScopes", "GroupsScopes", "NexusScopes", "SearchScopes", diff --git a/src/globus_sdk/scopes/data/auth.py b/src/globus_sdk/scopes/data/auth.py index 6386f75f5..9d8e9371d 100644 --- a/src/globus_sdk/scopes/data/auth.py +++ b/src/globus_sdk/scopes/data/auth.py @@ -1,23 +1,18 @@ -from ..builder import ScopeBuilder +from ..collection import StaticScopeCollection, _urn_scope +from ..representation import Scope -class _AuthScopesBuilder(ScopeBuilder): - _classattr_scope_names = ["openid", "email", "profile"] +class AuthScopes(StaticScopeCollection): + resource_server = "auth.globus.org" - openid: str = "openid" - email: str = "email" - profile: str = "profile" + openid = Scope("openid") + email = Scope("email") + profile = Scope("profile") - -AuthScopes = _AuthScopesBuilder( - "auth.globus.org", - known_scopes=[ - "manage_projects", - "view_authentications", - "view_clients", - "view_clients_and_scopes", - "view_consents", - "view_identities", - "view_identity_set", - ], -) + manage_projects = _urn_scope(resource_server, "manage_projects") + view_authentications = _urn_scope(resource_server, "view_authentications") + view_clients = _urn_scope(resource_server, "view_clients") + view_clients_and_scopes = _urn_scope(resource_server, "view_clients_and_scopes") + view_consents = _urn_scope(resource_server, "view_consents") + view_identities = _urn_scope(resource_server, "view_identities") + view_identity_set = _urn_scope(resource_server, "view_identity_set") diff --git a/src/globus_sdk/scopes/data/compute.py b/src/globus_sdk/scopes/data/compute.py index 4f08ed0fe..77d55c05c 100644 --- a/src/globus_sdk/scopes/data/compute.py +++ b/src/globus_sdk/scopes/data/compute.py @@ -1,36 +1,10 @@ -from __future__ import annotations +from ..collection import StaticScopeCollection, _url_scope -from ..builder import ScopeBuilder, ScopeBuilderScopes +class ComputeScopes(StaticScopeCollection): + # The Compute service breaks the scopes/resource server convention: its resource + # server is a service name and its scopes are built around the client ID. + resource_server = "funcx_service" + client_id = "facd7ccc-c5f4-42aa-916b-a0e270e2c2a9" -class _ComputeScopeBuilder(ScopeBuilder): - """The Compute service breaks the scopes/resource server convention: its resource - server is a service name and its scopes are built around the client ID. - """ - - def __init__( - self, - resource_server: str, - client_id: str, - known_scopes: ScopeBuilderScopes = None, - known_url_scopes: ScopeBuilderScopes = None, - ) -> None: - self._client_id = client_id - super().__init__( - resource_server, - known_scopes=known_scopes, - known_url_scopes=known_url_scopes, - ) - - def urn_scope_string(self, scope_name: str) -> str: - return f"urn:globus:auth:scope:{self._client_id}:{scope_name}" - - def url_scope_string(self, scope_name: str) -> str: - return f"https://auth.globus.org/scopes/{self._client_id}/{scope_name}" - - -ComputeScopes = _ComputeScopeBuilder( - "funcx_service", - "facd7ccc-c5f4-42aa-916b-a0e270e2c2a9", - known_url_scopes=["all"], -) + all = _url_scope(client_id, "all") diff --git a/src/globus_sdk/scopes/data/flows.py b/src/globus_sdk/scopes/data/flows.py index 5149b1590..abbad7974 100644 --- a/src/globus_sdk/scopes/data/flows.py +++ b/src/globus_sdk/scopes/data/flows.py @@ -1,56 +1,34 @@ from __future__ import annotations import typing as t +from functools import cached_property from globus_sdk._types import UUIDLike -from ..builder import ScopeBuilder, ScopeBuilderScopes - - -class _FlowsScopeBuilder(ScopeBuilder): - """ - The Flows service breaks the scopes/resource server convention: its resource server - is a domain name but its scopes are built around the client ID. - - Given that there isn't a simple way to support this more generally (and we - shouldn't encourage supporting this more generally), this class serves to - build out the scopes accurately specifically for Flows. - """ - - def __init__( - self, - domain_name: str, - client_id: str, - known_scopes: ScopeBuilderScopes = None, - known_url_scopes: ScopeBuilderScopes = None, - ) -> None: - self._client_id = client_id - super().__init__( - domain_name, known_scopes=known_scopes, known_url_scopes=known_url_scopes - ) - - def urn_scope_string(self, scope_name: str) -> str: - return f"urn:globus:auth:scope:{self._client_id}:{scope_name}" +from ..collection import ( + DynamicScopeCollection, + StaticScopeCollection, + _url_scope, +) +from ..representation import Scope - def url_scope_string(self, scope_name: str) -> str: - return f"https://auth.globus.org/scopes/{self._client_id}/{scope_name}" +class FlowsScopes(StaticScopeCollection): + # The Flows service breaks the scopes/resource server convention: its + # resource server is a domain name but its scopes are built around the + # client ID. + resource_server = "flows.globus.org" + client_id = "eec9b274-0c81-4334-bdc2-54e90e689b9a" -FlowsScopes = _FlowsScopeBuilder( - "flows.globus.org", - "eec9b274-0c81-4334-bdc2-54e90e689b9a", - known_url_scopes=[ - "all", - "manage_flows", - "view_flows", - "run", - "run_status", - "run_manage", - ], -) + all = _url_scope(client_id, "all") + manage_flows = _url_scope(client_id, "manage_flows") + view_flows = _url_scope(client_id, "view_flows") + run = _url_scope(client_id, "run") + run_status = _url_scope(client_id, "run_status") + run_manage = _url_scope(client_id, "run_manage") -class _SpecificFlowScopesClassStub(ScopeBuilder): +class _SpecificFlowScopesClassStub(DynamicScopeCollection): """ This stub object ensures that the type deductions for type checkers (e.g. mypy) on SpecificFlowClient.scopes are correct. @@ -63,17 +41,14 @@ class _SpecificFlowScopesClassStub(ScopeBuilder): instance-var access. """ - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + _scope_names = ("user",) + + def __init__(self) -> None: super().__init__("") - def __getattr__(self, name: str) -> t.Any: + def __getattribute__(self, name: str) -> t.Any: if name == "user": _raise_attr_error("scopes") - elif name == "resource_server": - _raise_attr_error("resource_server") - return super().__getattr__(name) - - def __getattribute__(self, name: str) -> t.Any: if name == "resource_server": _raise_attr_error("resource_server") return object.__getattribute__(self, name) @@ -88,7 +63,7 @@ def _raise_attr_error(name: str) -> t.NoReturn: ) -class SpecificFlowScopeBuilder(ScopeBuilder): +class SpecificFlowScopes(DynamicScopeCollection): """ This defines the scopes for a single flow (as distinct from the Flows service). @@ -99,16 +74,20 @@ class SpecificFlowScopeBuilder(ScopeBuilder): .. code-block:: python - sb = SpecificFlowScopeBuilder("my-flow-id-here") - flow_scope = sb.user + sc = SpecificFlowScopes("my-flow-id-here") + flow_scope = sc.user """ _CLASS_STUB = _SpecificFlowScopesClassStub() + _scope_names = ("user",) def __init__(self, flow_id: UUIDLike) -> None: self._flow_id = flow_id - str_flow_id = str(flow_id) - super().__init__( - resource_server=str_flow_id, - known_url_scopes=[("user", f"flow_{str_flow_id.replace('-', '_')}_user")], + self._str_flow_id = str(flow_id) + super().__init__(resource_server=self._str_flow_id) + + @cached_property + def user(self) -> Scope: + return _url_scope( + self.resource_server, f"flow_{self._str_flow_id.replace('-', '_')}_user" ) diff --git a/src/globus_sdk/scopes/data/gcs.py b/src/globus_sdk/scopes/data/gcs.py index ea8cba698..6137d404f 100644 --- a/src/globus_sdk/scopes/data/gcs.py +++ b/src/globus_sdk/scopes/data/gcs.py @@ -1,44 +1,47 @@ -from ..builder import ScopeBuilder +from functools import cached_property +from ..collection import DynamicScopeCollection, _url_scope, _urn_scope +from ..representation import Scope -class GCSEndpointScopeBuilder(ScopeBuilder): + +class GCSEndpointScopes(DynamicScopeCollection): """ - A ScopeBuilder with a named property for the GCS manage_collections scope. - "manage_collections" is a scope on GCS Endpoints. The resource_server string should - be the GCS Endpoint ID. + A dynamic ScopeCollection with a named property for the GCS + manage_collections scope. "manage_collections" is a scope on GCS Endpoints. + The resource_server string should be the GCS Endpoint ID. **Examples** - >>> sb = GCSEndpointScopeBuilder("xyz") + >>> sc = GCSEndpointScopes("xyz") >>> mc_scope = sb.manage_collections """ - _classattr_scope_names = ["manage_collections"] + _scope_names = ("manage_collections",) - @property - def manage_collections(self) -> str: - return self.urn_scope_string("manage_collections") + @cached_property + def manage_collections(self) -> Scope: + return _urn_scope(self.resource_server, "manage_collections") -class GCSCollectionScopeBuilder(ScopeBuilder): +class GCSCollectionScopes(DynamicScopeCollection): """ - A ScopeBuilder with a named property for the GCS data_access scope. + A dynamic ScopeCollection with a named property for the GCS data_access scope. "data_access" is a scope on GCS Collections. The resource_server string should be the GCS Collection ID. **Examples** - >>> sb = GCSCollectionScopeBuilder("xyz") - >>> da_scope = sb.data_access - >>> https_scope = sb.https + >>> sc = GCSCollectionScopes("xyz") + >>> da_scope = sc.data_access + >>> https_scope = sc.https """ - _classattr_scope_names = ["data_access", "https"] + _scope_names = ("data_access", "https") - @property - def data_access(self) -> str: - return self.url_scope_string("data_access") + @cached_property + def data_access(self) -> Scope: + return _url_scope(self.resource_server, "data_access") - @property - def https(self) -> str: - return self.url_scope_string("https") + @cached_property + def https(self) -> Scope: + return _url_scope(self.resource_server, "https") diff --git a/src/globus_sdk/scopes/data/groups.py b/src/globus_sdk/scopes/data/groups.py index 89fdc6088..41b2e20a8 100644 --- a/src/globus_sdk/scopes/data/groups.py +++ b/src/globus_sdk/scopes/data/groups.py @@ -1,17 +1,16 @@ -from ..builder import ScopeBuilder +from ..collection import StaticScopeCollection, _urn_scope -GroupsScopes = ScopeBuilder( - "groups.api.globus.org", - known_scopes=[ - "all", - "view_my_groups_and_memberships", - ], -) +class GroupsScopes(StaticScopeCollection): + resource_server = "groups.api.globus.org" -NexusScopes = ScopeBuilder( - "nexus.api.globus.org", - known_scopes=[ - "groups", - ], -) + all = _urn_scope(resource_server, "all") + view_my_groups_and_memberships = _urn_scope( + resource_server, "view_my_groups_and_memberships" + ) + + +class NexusScopes(StaticScopeCollection): + resource_server = "nexus.api.globus.org" + + groups = _urn_scope(resource_server, "groups") diff --git a/src/globus_sdk/scopes/data/search.py b/src/globus_sdk/scopes/data/search.py index 2bc540fb3..0088d96f1 100644 --- a/src/globus_sdk/scopes/data/search.py +++ b/src/globus_sdk/scopes/data/search.py @@ -1,11 +1,10 @@ -from ..builder import ScopeBuilder +from ..collection import StaticScopeCollection, _urn_scope -SearchScopes = ScopeBuilder( - "search.api.globus.org", - known_scopes=[ - "all", - "globus_connect_server", - "ingest", - "search", - ], -) + +class SearchScopes(StaticScopeCollection): + resource_server = "search.api.globus.org" + + all = _urn_scope(resource_server, "all") + globus_connect_server = _urn_scope(resource_server, "globus_connect_server") + ingest = _urn_scope(resource_server, "ingest") + search = _urn_scope(resource_server, "search") diff --git a/src/globus_sdk/scopes/data/timers.py b/src/globus_sdk/scopes/data/timers.py index 62b97d192..75e506335 100644 --- a/src/globus_sdk/scopes/data/timers.py +++ b/src/globus_sdk/scopes/data/timers.py @@ -1,8 +1,7 @@ -from ..builder import ScopeBuilder +from ..collection import StaticScopeCollection, _url_scope -TimersScopes = ScopeBuilder( - "524230d7-ea86-4a52-8312-86065a9e0417", - known_url_scopes=[ - "timer", - ], -) + +class TimersScopes(StaticScopeCollection): + resource_server = "524230d7-ea86-4a52-8312-86065a9e0417" + + timer = _url_scope(resource_server, "timer") diff --git a/src/globus_sdk/scopes/data/transfer.py b/src/globus_sdk/scopes/data/transfer.py index 967262eb3..5d8c2288c 100644 --- a/src/globus_sdk/scopes/data/transfer.py +++ b/src/globus_sdk/scopes/data/transfer.py @@ -1,9 +1,8 @@ -from ..builder import ScopeBuilder +from ..collection import StaticScopeCollection, _urn_scope -TransferScopes = ScopeBuilder( - "transfer.api.globus.org", - known_scopes=[ - "all", - "gcp_install", - ], -) + +class TransferScopes(StaticScopeCollection): + resource_server = "transfer.api.globus.org" + + all = _urn_scope(resource_server, "all") + gcp_install = _urn_scope(resource_server, "gcp_install") diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 78ae68f44..6a56c8fea 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -104,9 +104,9 @@ class AuthClient(client.BaseClient): error_class = AuthAPIError scopes = AuthScopes default_scope_requirements = [ - Scope(AuthScopes.openid), - Scope(AuthScopes.profile), - Scope(AuthScopes.email), + AuthScopes.openid, + AuthScopes.profile, + AuthScopes.email, ] def __init__( diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index 374d84e2d..2ba9be311 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -7,7 +7,7 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike -from globus_sdk.scopes import ComputeScopes, Scope +from globus_sdk.scopes import ComputeScopes from .errors import ComputeAPIError @@ -26,7 +26,7 @@ class ComputeClientV2(client.BaseClient): error_class = ComputeAPIError service_name = "compute" scopes = ComputeScopes - default_scope_requirements = [Scope(ComputeScopes.all)] + default_scope_requirements = [ComputeScopes.all] def get_version(self, service: str | MissingType = MISSING) -> GlobusHTTPResponse: """Get the current version of the API and other services. @@ -269,7 +269,7 @@ class ComputeClientV3(client.BaseClient): error_class = ComputeAPIError service_name = "compute" scopes = ComputeScopes - default_scope_requirements = [Scope(ComputeScopes.all)] + default_scope_requirements = [ComputeScopes.all] def register_endpoint(self, data: dict[str, t.Any]) -> GlobusHTTPResponse: """Register a new endpoint. diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 803b22cfa..7d5044233 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -20,10 +20,9 @@ from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import ( FlowsScopes, - GCSCollectionScopeBuilder, + GCSCollectionScopes, Scope, - ScopeBuilder, - SpecificFlowScopeBuilder, + SpecificFlowScopes, TransferScopes, ) @@ -55,7 +54,7 @@ class FlowsClient(client.BaseClient): error_class = FlowsAPIError service_name = "flows" scopes = FlowsScopes - default_scope_requirements = [Scope(FlowsScopes.all)] + default_scope_requirements = [FlowsScopes.all] def create_flow( self, @@ -927,7 +926,9 @@ class SpecificFlowClient(client.BaseClient): error_class = FlowsAPIError service_name = "flows" - scopes: ScopeBuilder = SpecificFlowScopeBuilder._CLASS_STUB + scopes: SpecificFlowScopes = ( + SpecificFlowScopes._CLASS_STUB # type: ignore[assignment] + ) def __init__( self, @@ -941,7 +942,7 @@ def __init__( transport_params: dict[str, t.Any] | None = None, ) -> None: self._flow_id = flow_id - self.scopes = SpecificFlowScopeBuilder(flow_id) + self.scopes = SpecificFlowScopes(flow_id) super().__init__( app=app, app_scopes=app_scopes, @@ -953,7 +954,7 @@ def __init__( @property def default_scope_requirements(self) -> list[Scope]: - return [Scope(self.scopes.user)] + return [self.scopes.user] def add_app_transfer_data_access_scope( self, collection_ids: UUIDLike | t.Iterable[UUIDLike] @@ -1004,15 +1005,14 @@ def add_app_transfer_data_access_scope( for i, c in enumerate(collection_ids_): _guards.validators.uuidlike(f"collection_ids[{i}]", c) - transfer_scope = Scope(TransferScopes.all, optional=True) + transfer_scope = TransferScopes.all.with_optional(True) for coll_id in collection_ids_: - data_access_scope = Scope( - GCSCollectionScopeBuilder(str(coll_id)).data_access, - optional=True, - ) + data_access_scope = GCSCollectionScopes( + str(coll_id) + ).data_access.with_optional(True) transfer_scope = transfer_scope.with_dependency(data_access_scope) - specific_flow_scope = Scope(self.scopes.user, dependencies=(transfer_scope,)) + specific_flow_scope = self.scopes.user.with_dependency(transfer_scope) self.add_app_scope(specific_flow_scope) return self diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index fb3449030..baf5b6184 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -3,7 +3,7 @@ import typing as t import uuid -from globus_sdk import client, exc, paging, response, scopes +from globus_sdk import client, exc, paging, response from globus_sdk._classproperty import classproperty from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin @@ -11,7 +11,7 @@ from globus_sdk._utils import slash_join from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp -from globus_sdk.scopes import Scope +from globus_sdk.scopes import GCSCollectionScopes, GCSEndpointScopes, Scope from .connector_table import ConnectorTable from .data import ( @@ -84,30 +84,30 @@ def __init__( @staticmethod def get_gcs_endpoint_scopes( endpoint_id: uuid.UUID | str, - ) -> scopes.GCSEndpointScopeBuilder: + ) -> GCSEndpointScopes: """Given a GCS Endpoint ID, this helper constructs an object containing the scopes for that Endpoint. :param endpoint_id: The ID of the Endpoint - See documentation for :class:`globus_sdk.scopes.GCSEndpointScopeBuilder` for + See documentation for :class:`globus_sdk.scopes.GCSEndpointScopes` for more information. """ - return scopes.GCSEndpointScopeBuilder(str(endpoint_id)) + return GCSEndpointScopes(str(endpoint_id)) @staticmethod def get_gcs_collection_scopes( collection_id: uuid.UUID | str, - ) -> scopes.GCSCollectionScopeBuilder: + ) -> GCSCollectionScopes: """Given a GCS Collection ID, this helper constructs an object containing the scopes for that Collection. :param collection_id: The ID of the Collection - See documentation for :class:`globus_sdk.scopes.GCSCollectionScopeBuilder` for + See documentation for :class:`globus_sdk.scopes.GCSCollectionScopes` for more information. """ - return scopes.GCSCollectionScopeBuilder(str(collection_id)) + return GCSCollectionScopes(str(collection_id)) @staticmethod def connector_id_to_name(connector_id: UUIDLike) -> str | None: @@ -140,11 +140,9 @@ def connector_id_to_name(connector_id: UUIDLike) -> str | None: @property def default_scope_requirements(self) -> list[Scope]: return [ - Scope( - GCSClient.get_gcs_endpoint_scopes( - self.endpoint_client_id - ).manage_collections - ) + GCSClient.get_gcs_endpoint_scopes( + self.endpoint_client_id + ).manage_collections ] @classproperty diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index 48531007e..8ac13bce6 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -32,7 +32,7 @@ class GroupsClient(client.BaseClient): @property def default_scope_requirements(self) -> list[Scope]: - return [Scope(GroupsScopes.view_my_groups_and_memberships)] + return [GroupsScopes.view_my_groups_and_memberships] def get_my_groups( self, *, query_params: dict[str, t.Any] | None = None diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index 864048dbd..df84b4391 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -8,7 +8,7 @@ from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.exc.warnings import warn_deprecated -from globus_sdk.scopes import Scope, SearchScopes +from globus_sdk.scopes import SearchScopes from .data import SearchQuery, SearchScrollQuery from .errors import SearchAPIError @@ -33,15 +33,12 @@ class SearchClient(client.BaseClient): error_class = SearchAPIError service_name = "search" scopes = SearchScopes + default_scope_requirements = [SearchScopes.search] # # Index Management # - @property - def default_scope_requirements(self) -> list[Scope]: - return [Scope(SearchScopes.search)] - def create_index( self, display_name: str, description: str ) -> response.GlobusHTTPResponse: diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index de6f6a8a9..d57bf3b1b 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -7,7 +7,7 @@ from globus_sdk import _guards, client, exc, response from globus_sdk._types import UUIDLike from globus_sdk.scopes import ( - GCSCollectionScopeBuilder, + GCSCollectionScopes, Scope, TimersScopes, TransferScopes, @@ -31,7 +31,7 @@ class TimersClient(client.BaseClient): error_class = TimersAPIError service_name = "timer" scopes = TimersScopes - default_scope_requirements = [Scope(TimersScopes.timer)] + default_scope_requirements = [TimersScopes.timer] def add_app_transfer_data_access_scope( self, collection_ids: UUIDLike | t.Iterable[UUIDLike] @@ -86,17 +86,15 @@ def add_app_transfer_data_access_scope( for i, c in enumerate(collection_ids_): _guards.validators.uuidlike(f"collection_ids[{i}]", c) - transfer_scope = Scope(TransferScopes.all) dependencies: list[Scope] = [] for coll_id in collection_ids_: - data_access_scope = Scope( - GCSCollectionScopeBuilder(str(coll_id)).data_access, - optional=True, - ) + data_access_scope = GCSCollectionScopes( + str(coll_id) + ).data_access.with_optional(True) dependencies.append(data_access_scope) - transfer_scope = transfer_scope.with_dependencies(dependencies) + transfer_scope = TransferScopes.all.with_dependencies(dependencies) - timers_scope = Scope(TimersScopes.timer, dependencies=(transfer_scope,)) + timers_scope = TimersScopes.timer.with_dependency(transfer_scope) self.add_app_scope(timers_scope) return self diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 64ab05554..60c10a7ed 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -9,7 +9,7 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin from globus_sdk._types import DateLike, IntLike, UUIDLike -from globus_sdk.scopes import GCSCollectionScopeBuilder, Scope, TransferScopes +from globus_sdk.scopes import GCSCollectionScopes, Scope, TransferScopes from .data import DeleteData, TransferData from .errors import TransferAPIError @@ -131,7 +131,7 @@ class TransferClient(client.BaseClient): transport_class: type[TransferRequestsTransport] = TransferRequestsTransport error_class = TransferAPIError scopes = TransferScopes - default_scope_requirements = [Scope(TransferScopes.all)] + default_scope_requirements = [TransferScopes.all] def add_app_data_access_scope( self, collection_ids: UUIDLike | t.Iterable[UUIDLike] @@ -198,13 +198,12 @@ def add_app_data_access_scope( for i, c in enumerate(collection_ids_): _guards.validators.uuidlike(f"collection_ids[{i}]", c) - scope = Scope(TransferScopes.all) + scope = TransferScopes.all dependencies: list[Scope] = [] for coll_id in collection_ids_: - data_access_scope = Scope( - GCSCollectionScopeBuilder(str(coll_id)).data_access, - optional=True, - ) + data_access_scope = GCSCollectionScopes( + str(coll_id) + ).data_access.with_optional(True) dependencies.append(data_access_scope) scope = scope.with_dependencies(dependencies) self.add_app_scope(scope) diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index 87cac2678..75cbbb672 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -196,7 +196,7 @@ def test_oauth2_get_authorize_url_native_defaults(native_client): assert parsed_params == { "client_id": [native_client.client_id], "redirect_uri": [native_client.base_url + "v2/web/auth-code"], - "scope": [TransferScopes.all], + "scope": [str(TransferScopes.all)], "state": ["_default"], "response_type": ["code"], "code_challenge": [flow_manager.challenge], @@ -251,7 +251,7 @@ def test_oauth2_get_authorize_url_confidential_defaults(confidential_client): assert parsed_params == { "client_id": [confidential_client.client_id], "redirect_uri": ["uri"], - "scope": [TransferScopes.all], + "scope": [str(TransferScopes.all)], "state": ["_default"], "response_type": ["code"], "access_type": ["online"], diff --git a/tests/functional/services/gcs/test_scope_helpers.py b/tests/functional/services/gcs/test_scope_helpers.py index 747411e32..c0c532041 100644 --- a/tests/functional/services/gcs/test_scope_helpers.py +++ b/tests/functional/services/gcs/test_scope_helpers.py @@ -5,29 +5,32 @@ def test_manage_collections_scope_helper(client): - sb = client.get_gcs_endpoint_scopes(zero_id) + sc = client.get_gcs_endpoint_scopes(zero_id) assert ( - sb.manage_collections == f"urn:globus:auth:scope:{zero_id_s}:manage_collections" + str(sc.manage_collections) + == f"urn:globus:auth:scope:{zero_id_s}:manage_collections" ) # data_access is separated from endpoint scopes - assert not hasattr(sb, "data_access") + assert not hasattr(sc, "data_access") def test_data_access_scope_helper(client): - sb = client.get_gcs_collection_scopes(zero_id) - assert sb.data_access == f"https://auth.globus.org/scopes/{zero_id_s}/data_access" - assert sb.https == f"https://auth.globus.org/scopes/{zero_id_s}/https" + sc = client.get_gcs_collection_scopes(zero_id) + assert ( + str(sc.data_access) == f"https://auth.globus.org/scopes/{zero_id_s}/data_access" + ) + assert str(sc.https) == f"https://auth.globus.org/scopes/{zero_id_s}/https" # manage_collections is separated from collection scopes - assert not hasattr(sb, "manage_collections") + assert not hasattr(sc, "manage_collections") def test_str_contains_scope_properties(client): - ep_sb = client.get_gcs_endpoint_scopes(zero_id) + ep_sc = client.get_gcs_endpoint_scopes(zero_id) - assert "manage_collections" in str(ep_sb) - assert ep_sb.manage_collections in str(ep_sb) + assert "manage_collections" in str(ep_sc) + assert str(ep_sc.manage_collections) in str(ep_sc) - collection_sb = client.get_gcs_collection_scopes(zero_id) + collection_sc = client.get_gcs_collection_scopes(zero_id) - assert "data_access" in str(collection_sb) - assert collection_sb.data_access in str(collection_sb) + assert "data_access" in str(collection_sc) + assert str(collection_sc.data_access) in str(collection_sc) diff --git a/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py b/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py index 2042c6d0e..32bb7aef3 100644 --- a/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py +++ b/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py @@ -8,7 +8,7 @@ specific_flow_client = globus_sdk.SpecificFlowClient(flow_id) scopes_object = specific_flow_client.scopes -t.assert_type(scopes_object, globus_sdk.scopes.ScopeBuilder) +t.assert_type(scopes_object, globus_sdk.scopes.DynamicScopeCollection) scope: str = scopes_object.user x: int = scopes_object.user # type: ignore[assignment] diff --git a/tests/unit/globus_app/test_client_integration.py b/tests/unit/globus_app/test_client_integration.py index 0e1433b29..126761f8d 100644 --- a/tests/unit/globus_app/test_client_integration.py +++ b/tests/unit/globus_app/test_client_integration.py @@ -103,7 +103,7 @@ def test_transfer_client_add_app_data_access_scope_in_iterable(app): transfer_dependencies = [] for scope in app.scope_requirements["transfer.api.globus.org"]: - if scope.scope_string != globus_sdk.TransferClient.scopes.all: + if scope.scope_string != str(globus_sdk.TransferClient.scopes.all): continue for dep in scope.dependencies: transfer_dependencies.append((dep.scope_string, dep.optional)) @@ -124,10 +124,10 @@ def test_timers_client_add_app_data_access_scope_in_iterable(app): transfer_dependencies = [] for scope in app.scope_requirements[globus_sdk.TimersClient.resource_server]: - if scope.scope_string != globus_sdk.TimersClient.scopes.timer: + if scope.scope_string != str(globus_sdk.TimersClient.scopes.timer): continue for dep in scope.dependencies: - if dep.scope_string != globus_sdk.TransferClient.scopes.all: + if dep.scope_string != str(globus_sdk.TransferClient.scopes.all): continue for subdep in dep.dependencies: transfer_dependencies.append((subdep.scope_string, subdep.optional)) diff --git a/tests/unit/scopes/test_scope_builder.py b/tests/unit/scopes/test_scope_builder.py deleted file mode 100644 index 37d078c8b..000000000 --- a/tests/unit/scopes/test_scope_builder.py +++ /dev/null @@ -1,107 +0,0 @@ -import uuid - -from globus_sdk.scopes import ComputeScopes, FlowsScopes, ScopeBuilder - - -def test_url_scope_string(): - sb = ScopeBuilder(str(uuid.UUID(int=0))) - assert sb.url_scope_string("data_access") == ( - "https://auth.globus.org/scopes/00000000-0000-0000-0000-000000000000" - "/data_access" - ) - - -def test_urn_scope_string(): - sb = ScopeBuilder("example.globus.org") - assert ( - sb.urn_scope_string("scope") == "urn:globus:auth:scope:example.globus.org:scope" - ) - - -def test_known_scopes(): - sb = ScopeBuilder(str(uuid.UUID(int=0)), known_scopes="foo") - assert sb.foo == "urn:globus:auth:scope:00000000-0000-0000-0000-000000000000:foo" - - -def test_known_url_scopes(): - sb = ScopeBuilder(str(uuid.UUID(int=0)), known_url_scopes="foo") - assert sb.foo == ( - "https://auth.globus.org/scopes/00000000-0000-0000-0000-000000000000/foo" - ) - - -def test_scopebuilder_str(): - sb = ScopeBuilder(str(uuid.UUID(int=0)), known_scopes="foo", known_url_scopes="bar") - rs, foo_scope, bar_scope = sb.resource_server, sb.foo, sb.bar - - stringified = str(sb) - assert rs in stringified - assert foo_scope in stringified - assert bar_scope in stringified - - -def test_uniquely_named_scopes(): - rs = str(uuid.uuid4()) - scope_1 = str(uuid.uuid4()) - scope_2 = str(uuid.uuid4()) - sb = ScopeBuilder( - rs, - known_scopes=[("my_urn_scope", scope_1), "foo"], - known_url_scopes=[("my_url_scope", scope_2), "bar"], - ) - - assert sb.my_urn_scope == f"urn:globus:auth:scope:{rs}:{scope_1}" - assert sb.foo == f"urn:globus:auth:scope:{rs}:foo" - assert sb.my_url_scope == f"https://auth.globus.org/scopes/{rs}/{scope_2}" - assert sb.bar == f"https://auth.globus.org/scopes/{rs}/bar" - - -def test_sb_allowed_inputs_types(): - rs = str(uuid.uuid4()) - scope_1 = "do_a_thing" - scope_1_urn = f"urn:globus:auth:scope:{rs}:{scope_1}" - - none_sb = ScopeBuilder(rs, known_scopes=None) - str_sb = ScopeBuilder(rs, known_scopes=scope_1) - tuple_sb = ScopeBuilder(rs, known_scopes=("scope_1", scope_1)) - list_sb = ScopeBuilder(rs, known_scopes=[scope_1, ("scope_1", scope_1)]) - - assert none_sb.scope_names == [] - assert scope_1 in str_sb.scope_names - assert str_sb.do_a_thing == scope_1_urn - assert "scope_1" in tuple_sb.scope_names - assert tuple_sb.scope_1 == scope_1_urn - assert scope_1 in list_sb.scope_names - assert "scope_1" in list_sb.scope_names - assert list_sb.scope_1 == scope_1_urn - assert list_sb.do_a_thing == scope_1_urn - - -def test_flows_scopes_creation(): - assert FlowsScopes.resource_server == "flows.globus.org" - assert ( - FlowsScopes.run - == "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/run" - ) - - -def test_compute_scopes_creation(): - assert ComputeScopes.resource_server == "funcx_service" - assert ( - ComputeScopes.all - == "https://auth.globus.org/scopes/facd7ccc-c5f4-42aa-916b-a0e270e2c2a9/all" - ) - - -def test_stringify_scope_builder(): - class MyScopeBuilder(ScopeBuilder): - pass - - sb = MyScopeBuilder("foo", known_scopes=["sc1"]) - assert ( - str(sb) - == """\ -MyScopeBuilder[foo] - sc1: - urn:globus:auth:scope:foo:sc1""" - ) diff --git a/tests/unit/scopes/test_scope_collections.py b/tests/unit/scopes/test_scope_collections.py new file mode 100644 index 000000000..fcc9423bd --- /dev/null +++ b/tests/unit/scopes/test_scope_collections.py @@ -0,0 +1,90 @@ +import textwrap +import uuid + +from globus_sdk.scopes import ( + ComputeScopes, + DynamicScopeCollection, + FlowsScopes, + Scope, + StaticScopeCollection, +) +from globus_sdk.scopes.collection import _url_scope, _urn_scope + + +def test_url_scope_string(): + resource_server = str(uuid.UUID(int=0)) + s = _url_scope(resource_server, "data_access") + assert isinstance(s, Scope) + assert str(s) == ( + "https://auth.globus.org/scopes/00000000-0000-0000-0000-000000000000" + "/data_access" + ) + + +def test_urn_scope_string(): + resource_server = "example.globus.org" + s = _urn_scope(resource_server, "myscope") + assert isinstance(s, Scope) + assert str(s) == "urn:globus:auth:scope:example.globus.org:myscope" + + +def test_static_scope_collection_str_contains_expected_values(): + class MyScopes(StaticScopeCollection): + resource_server = str(uuid.UUID(int=0)) + + foo = _urn_scope(resource_server, "foo") + bar = _url_scope(resource_server, "bar") + + stringified = str(MyScopes) + assert MyScopes.resource_server in stringified + assert str(MyScopes.foo) in stringified + assert str(MyScopes.bar) in stringified + + +def test_dynamic_scope_collection_contains_expected_values(): + class MyScopes(DynamicScopeCollection): + _scope_names = ("foo", "bar") + + @property + def foo(self): + return _urn_scope(self.resource_server, "foo") + + @property + def bar(self): + return _url_scope(self.resource_server, "bar") + + resource_server = str(uuid.UUID(int=10)) + scope_collection = MyScopes(resource_server) + stringified = str(scope_collection) + assert scope_collection.resource_server in stringified + assert str(scope_collection.foo) in stringified + assert str(scope_collection.bar) in stringified + + +def test_flows_scopes_creation(): + assert FlowsScopes.resource_server == "flows.globus.org" + assert ( + str(FlowsScopes.run) + == "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/run" + ) + + +def test_compute_scopes_creation(): + assert ComputeScopes.resource_server == "funcx_service" + assert ( + str(ComputeScopes.all) + == "https://auth.globus.org/scopes/facd7ccc-c5f4-42aa-916b-a0e270e2c2a9/all" + ) + + +def test_stringify_static_scope_collection(): + class MyScopes(StaticScopeCollection): + resource_server = "foo" + sc1 = _urn_scope(resource_server, "sc1") + + assert str(MyScopes) == textwrap.dedent( + """\ + MyScopes[foo] + sc1: + urn:globus:auth:scope:foo:sc1""" + ) diff --git a/tests/unit/sphinxext/test_list_known_scopes.py b/tests/unit/sphinxext/test_list_known_scopes.py index 2710178fa..bb105bfd9 100644 --- a/tests/unit/sphinxext/test_list_known_scopes.py +++ b/tests/unit/sphinxext/test_list_known_scopes.py @@ -13,18 +13,18 @@ def test_listknownscopes_rejects_wrong_object_type(sphinx_runner, capsys): test_line = None for line in err_lines: - if "ScopeBuilder" in line: + if "scope collection" in line: test_line = line break else: - pytest.fail("Didn't find 'ScopeBuilder' in stderr") + pytest.fail("Didn't find 'scope collection' in stderr") assert re.search( - r"Expected to be a ScopeBuilder", test_line + r"Expected to be a scope collection", test_line ) -# choose an arbitrary scope builder from the SDK and confirm that listknownscopes +# choose an arbitrary scope collection from the SDK and confirm that listknownscopes # will render its list of members # for this case, we're using `TimersScopes` def test_listknownscopes_of_timers(sphinx_runner): diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index 182592670..e6555262e 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -24,7 +24,7 @@ class CustomClient(globus_sdk.BaseClient): service_name = "transfer" transport_class = no_retry_transport scopes = TransferScopes - default_scope_requirements = [Scope(TransferScopes.all)] + default_scope_requirements = [TransferScopes.all] return CustomClient @@ -223,7 +223,7 @@ def _reraise_token_error(_: GlobusApp, error: TokenValidationError): # confirm default_required_scopes were automatically added assert [str(s) for s in app.scope_requirements[c.resource_server]] == [ - TransferScopes.all + str(TransferScopes.all) ] # confirm attempt at getting an authorizer from app @@ -250,7 +250,7 @@ def test_add_app_scope(base_client_class): c.add_app_scope("foo") str_list = [str(s) for s in app.scope_requirements[c.resource_server]] assert len(str_list) == 2 - assert TransferScopes.all in str_list + assert str(TransferScopes.all) in str_list assert "foo" in str_list @@ -259,7 +259,7 @@ def test_add_app_scope_chaining(base_client_class): c = base_client_class(app=app).add_app_scope("foo").add_app_scope("bar") str_list = [str(s) for s in app.scope_requirements[c.resource_server]] assert len(str_list) == 3 - assert TransferScopes.all in str_list + assert str(TransferScopes.all) in str_list assert "foo" in str_list assert "bar" in str_list @@ -328,7 +328,7 @@ def test_cannot_attach_app_when_authorizer_was_provided(base_client_class): def test_cannot_attach_app_when_resource_server_is_not_resolvable(): class CustomClient(globus_sdk.BaseClient): service_name = "transfer" - default_scope_requirements = [Scope(TransferScopes.all)] + default_scope_requirements = [TransferScopes.all] c = CustomClient() app = UserApp("SDK Test", client_id="client_id") diff --git a/tests/unit/test_specific_flows_client.py b/tests/unit/test_specific_flows_client.py index 531939c6f..d7485c312 100644 --- a/tests/unit/test_specific_flows_client.py +++ b/tests/unit/test_specific_flows_client.py @@ -1,6 +1,7 @@ import pytest import globus_sdk +from globus_sdk.scopes import Scope def test_specific_flow_client_class_errors_on_scope_access(): @@ -22,7 +23,7 @@ def test_specific_flow_client_class_errors_on_scope_access(): scopes.demuddle err = excinfo.value - assert str(err) == "Unrecognized Attribute 'demuddle'" + assert str(err).endswith("has no attribute 'demuddle'") def test_specific_flow_client_class_errors_on_resource_server_access(): @@ -57,15 +58,15 @@ def test_specific_flow_client_instance_supports_scope_access(): # for the 'user' scope, we get a string user_scope = scopes.user - assert isinstance(user_scope, str) - assert user_scope.endswith("flow_foo_user") + assert isinstance(user_scope, Scope) + assert str(user_scope).endswith("flow_foo_user") # but for any other scope we still get the generic attribute error with pytest.raises(AttributeError) as excinfo: scopes.demuddle err = excinfo.value - assert str(err) == "Unrecognized Attribute 'demuddle'" + assert str(err).endswith("has no attribute 'demuddle'") def test_specific_flow_client_instance_supports_resource_server_access(): From a50b499f122cfee7b493a60372f441db51f814cd Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 25 Jun 2025 12:26:44 -0500 Subject: [PATCH 071/176] Simplify the behavior of ScopeCollection types - Rather than using a metaclass to customize behavior, push "down" the type hierarchy by making each collection object an instance of the appropriate class. - Do not implement `__str__`, provide only `__iter__` for better object-oriented introspection needs. - Improve the specific flow scopes stub type to be a subclass of SpecificFlowScopes, allowing for simpler annotations. - Expose the `ScopeCollection` base type, not just the two subtypes. --- .../scopes_and_consents/scopes.rst | 4 + .../directives/list_known_scopes.py | 8 +- src/globus_sdk/client.py | 4 +- src/globus_sdk/scopes/__init__.py | 3 +- src/globus_sdk/scopes/collection.py | 61 ++++++++------- src/globus_sdk/scopes/data/auth.py | 5 +- src/globus_sdk/scopes/data/compute.py | 5 +- src/globus_sdk/scopes/data/flows.py | 75 ++++++++++--------- src/globus_sdk/scopes/data/groups.py | 8 +- src/globus_sdk/scopes/data/search.py | 5 +- src/globus_sdk/scopes/data/timers.py | 5 +- src/globus_sdk/scopes/data/transfer.py | 5 +- src/globus_sdk/services/flows/client.py | 4 +- .../scopes/test_scope_data_behaviors.py | 64 ++++++++++++++++ .../services/gcs/test_scope_helpers.py | 10 +-- .../mypy-ignore-tests/specific_flow_scopes.py | 4 +- tests/unit/scopes/test_scope_collections.py | 46 +++++------- 17 files changed, 194 insertions(+), 122 deletions(-) create mode 100644 tests/functional/scopes/test_scope_data_behaviors.py diff --git a/docs/authorization/scopes_and_consents/scopes.rst b/docs/authorization/scopes_and_consents/scopes.rst index cafd1b724..383537d22 100644 --- a/docs/authorization/scopes_and_consents/scopes.rst +++ b/docs/authorization/scopes_and_consents/scopes.rst @@ -205,6 +205,10 @@ ScopeBuilders Scope Collection Types ~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: ScopeCollection + :members: + :show-inheritance: + .. autoclass:: StaticScopeCollection :members: :show-inheritance: diff --git a/src/globus_sdk/_sphinxext/directives/list_known_scopes.py b/src/globus_sdk/_sphinxext/directives/list_known_scopes.py index 33bec88c8..a68e6bd29 100644 --- a/src/globus_sdk/_sphinxext/directives/list_known_scopes.py +++ b/src/globus_sdk/_sphinxext/directives/list_known_scopes.py @@ -5,7 +5,7 @@ from docutils.parsers.rst import directives -from globus_sdk.scopes import DynamicScopeCollection, StaticScopeCollection +from globus_sdk.scopes import Scope, ScopeCollection from .add_content_directive import AddContentDirective @@ -50,10 +50,8 @@ def gen_rst(self) -> t.Iterator[str]: def extract_known_scopes(scope_collection_name: str) -> list[str]: sc = locate(scope_collection_name) - if isinstance(sc, DynamicScopeCollection): - return list(sc._scope_names) - elif isinstance(sc, type) and issubclass(sc, StaticScopeCollection): - return list(sc._scope_names()) + if isinstance(sc, ScopeCollection): + return [name for name in dir(sc) if isinstance(getattr(sc, name), Scope)] raise RuntimeError( f"Expected {sc} to be a scope collection, but got {type(sc)} instead" diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index c802ff1ac..6b5cecedb 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -12,7 +12,7 @@ from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.paging import PaginatorTable from globus_sdk.response import GlobusHTTPResponse -from globus_sdk.scopes import DynamicScopeCollection, Scope, StaticScopeCollection +from globus_sdk.scopes import Scope, ScopeCollection from globus_sdk.transport import RequestsTransport if sys.version_info >= (3, 10): @@ -70,7 +70,7 @@ class BaseClient: transport_class: type[RequestsTransport] = RequestsTransport #: the scopes for this client may be present as a ``ScopeCollection`` - scopes: type[StaticScopeCollection] | DynamicScopeCollection | None = None + scopes: ScopeCollection | None = None def __init__( self, diff --git a/src/globus_sdk/scopes/__init__.py b/src/globus_sdk/scopes/__init__.py index f6fb960ec..21b476771 100644 --- a/src/globus_sdk/scopes/__init__.py +++ b/src/globus_sdk/scopes/__init__.py @@ -1,5 +1,5 @@ from ._normalize import scopes_to_scope_list, scopes_to_str -from .collection import DynamicScopeCollection, StaticScopeCollection +from .collection import DynamicScopeCollection, ScopeCollection, StaticScopeCollection from .data import ( AuthScopes, ComputeScopes, @@ -18,6 +18,7 @@ from .representation import Scope __all__ = ( + "ScopeCollection", "StaticScopeCollection", "DynamicScopeCollection", "Scope", diff --git a/src/globus_sdk/scopes/collection.py b/src/globus_sdk/scopes/collection.py index 20fa55e5c..0aee1d7d0 100644 --- a/src/globus_sdk/scopes/collection.py +++ b/src/globus_sdk/scopes/collection.py @@ -1,57 +1,51 @@ from __future__ import annotations +import abc import typing as t from .representation import Scope -class StaticScopeCollectionMeta(type): +class ScopeCollection(abc.ABC): """ - The metaclass for StaticScopeCollection. + The common base for scope collections. - This defines the the stringification of these classes. - """ + ScopeCollections act as namespaces with attribute access to get scopes. - def __str__(self) -> str: - name: str = getattr(self, "__name__", "") - resource_server: str = getattr( - self, "resource_server", "" - ) + They can also be iterated to get all of their defined scopes and provide + the appropriate resource_server string for use in OAuth2 flows. + """ - scope_names: t.Iterable[str] = self._scope_names() # type: ignore[attr-defined] + @property + @abc.abstractmethod + def resource_server(self) -> str: ... - return f"{name}[{resource_server}]\n" + "\n".join( - f" {name}:\n {getattr(self, name)}" for name in scope_names - ) + @abc.abstractmethod + def __iter__(self) -> t.Iterator[Scope]: ... -class StaticScopeCollection(metaclass=StaticScopeCollectionMeta): +class StaticScopeCollection(ScopeCollection): """ A static scope collection is a data container which provides various scopes as class attributes. - ``resource_server`` is available as a class attribute. - - ``str()`` is well-defined to produce a nice rendering of the scopes - contained in the class. + ``resource_server`` must be available as a class attribute. """ resource_server: t.ClassVar[str] - @classmethod - def _scope_names(cls) -> t.Iterator[str]: - for key, value in vars(cls).items(): - if isinstance(value, Scope): - yield key + def __iter__(self) -> t.Iterator[Scope]: + for view in (vars(self).values(), vars(self.__class__).values()): + for value in view: + if isinstance(value, Scope): + yield value -class DynamicScopeCollection: +class DynamicScopeCollection(ScopeCollection): """ The base type for dynamic scope collections, where the resource server is variable. - The class itself is not usable as a collection type, but its instances are. - The default implementation takes the resource server as the only init-time parameter. @@ -65,12 +59,17 @@ class DynamicScopeCollection: _scope_names: t.ClassVar[tuple[str, ...]] def __init__(self, resource_server: str) -> None: - self.resource_server = resource_server + self._resource_server = resource_server + + def __iter__(self) -> t.Iterator[Scope]: + for name in self._scope_names: + value = getattr(self, name) + if isinstance(value, Scope): + yield value - def __str__(self) -> str: - return f"{self.__class__.__name__}[{self.resource_server}]\n" + "\n".join( - f" {name}:\n {getattr(self, name)}" for name in self._scope_names - ) + @property + def resource_server(self) -> str: + return self._resource_server def _urn_scope(resource_server: str, scope_name: str) -> Scope: diff --git a/src/globus_sdk/scopes/data/auth.py b/src/globus_sdk/scopes/data/auth.py index 9d8e9371d..fc2afb0a9 100644 --- a/src/globus_sdk/scopes/data/auth.py +++ b/src/globus_sdk/scopes/data/auth.py @@ -2,7 +2,7 @@ from ..representation import Scope -class AuthScopes(StaticScopeCollection): +class _AuthScopes(StaticScopeCollection): resource_server = "auth.globus.org" openid = Scope("openid") @@ -16,3 +16,6 @@ class AuthScopes(StaticScopeCollection): view_consents = _urn_scope(resource_server, "view_consents") view_identities = _urn_scope(resource_server, "view_identities") view_identity_set = _urn_scope(resource_server, "view_identity_set") + + +AuthScopes = _AuthScopes() diff --git a/src/globus_sdk/scopes/data/compute.py b/src/globus_sdk/scopes/data/compute.py index 77d55c05c..9908c4534 100644 --- a/src/globus_sdk/scopes/data/compute.py +++ b/src/globus_sdk/scopes/data/compute.py @@ -1,10 +1,13 @@ from ..collection import StaticScopeCollection, _url_scope -class ComputeScopes(StaticScopeCollection): +class _ComputeScopes(StaticScopeCollection): # The Compute service breaks the scopes/resource server convention: its resource # server is a service name and its scopes are built around the client ID. resource_server = "funcx_service" client_id = "facd7ccc-c5f4-42aa-916b-a0e270e2c2a9" all = _url_scope(client_id, "all") + + +ComputeScopes = _ComputeScopes() diff --git a/src/globus_sdk/scopes/data/flows.py b/src/globus_sdk/scopes/data/flows.py index abbad7974..7d698137e 100644 --- a/src/globus_sdk/scopes/data/flows.py +++ b/src/globus_sdk/scopes/data/flows.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing as t -from functools import cached_property from globus_sdk._types import UUIDLike @@ -13,7 +12,7 @@ from ..representation import Scope -class FlowsScopes(StaticScopeCollection): +class _FlowsScopes(StaticScopeCollection): # The Flows service breaks the scopes/resource server convention: its # resource server is a domain name but its scopes are built around the # client ID. @@ -28,7 +27,45 @@ class FlowsScopes(StaticScopeCollection): run_manage = _url_scope(client_id, "run_manage") -class _SpecificFlowScopesClassStub(DynamicScopeCollection): +FlowsScopes = _FlowsScopes() + + +class SpecificFlowScopes(DynamicScopeCollection): + """ + This defines the scopes for a single flow (as distinct from the Flows service). + + It primarily provides the `user` scope which is typically needed to start a run of + a flow. + + Example usage: + + .. code-block:: python + + sc = SpecificFlowScopes("my-flow-id-here") + flow_scope = sc.user + """ + + _scope_names = ("user",) + + def __init__(self, flow_id: UUIDLike) -> None: + _flow_id = str(flow_id) + super().__init__(_flow_id) + + self.user: Scope = _url_scope( + _flow_id, f"flow_{_flow_id.replace('-', '_')}_user" + ) + + @classmethod + def _build_class_stub(cls) -> SpecificFlowScopes: + """ + This internal helper builds a "stub" object so that + ``SpecificFlowClient.scopes`` is typed as ``SpecificFlowScopes`` but + raises appropriate errors at runtime access via the class. + """ + return _SpecificFlowScopesClassStub() + + +class _SpecificFlowScopesClassStub(SpecificFlowScopes): """ This stub object ensures that the type deductions for type checkers (e.g. mypy) on SpecificFlowClient.scopes are correct. @@ -41,8 +78,6 @@ class _SpecificFlowScopesClassStub(DynamicScopeCollection): instance-var access. """ - _scope_names = ("user",) - def __init__(self) -> None: super().__init__("") @@ -61,33 +96,3 @@ def _raise_attr_error(name: str) -> t.NoReturn: f"Instead, instantiate a SpecificFlowClient and access the '{name}' attribute " "from that instance." ) - - -class SpecificFlowScopes(DynamicScopeCollection): - """ - This defines the scopes for a single flow (as distinct from the Flows service). - - It primarily provides the `user` scope which is typically needed to start a run of - a flow. - - Example usage: - - .. code-block:: python - - sc = SpecificFlowScopes("my-flow-id-here") - flow_scope = sc.user - """ - - _CLASS_STUB = _SpecificFlowScopesClassStub() - _scope_names = ("user",) - - def __init__(self, flow_id: UUIDLike) -> None: - self._flow_id = flow_id - self._str_flow_id = str(flow_id) - super().__init__(resource_server=self._str_flow_id) - - @cached_property - def user(self) -> Scope: - return _url_scope( - self.resource_server, f"flow_{self._str_flow_id.replace('-', '_')}_user" - ) diff --git a/src/globus_sdk/scopes/data/groups.py b/src/globus_sdk/scopes/data/groups.py index 41b2e20a8..cde841b39 100644 --- a/src/globus_sdk/scopes/data/groups.py +++ b/src/globus_sdk/scopes/data/groups.py @@ -1,7 +1,7 @@ from ..collection import StaticScopeCollection, _urn_scope -class GroupsScopes(StaticScopeCollection): +class _GroupsScopes(StaticScopeCollection): resource_server = "groups.api.globus.org" all = _urn_scope(resource_server, "all") @@ -10,7 +10,11 @@ class GroupsScopes(StaticScopeCollection): ) -class NexusScopes(StaticScopeCollection): +class _NexusScopes(StaticScopeCollection): resource_server = "nexus.api.globus.org" groups = _urn_scope(resource_server, "groups") + + +GroupsScopes = _GroupsScopes() +NexusScopes = _NexusScopes() diff --git a/src/globus_sdk/scopes/data/search.py b/src/globus_sdk/scopes/data/search.py index 0088d96f1..6a88115ec 100644 --- a/src/globus_sdk/scopes/data/search.py +++ b/src/globus_sdk/scopes/data/search.py @@ -1,10 +1,13 @@ from ..collection import StaticScopeCollection, _urn_scope -class SearchScopes(StaticScopeCollection): +class _SearchScopes(StaticScopeCollection): resource_server = "search.api.globus.org" all = _urn_scope(resource_server, "all") globus_connect_server = _urn_scope(resource_server, "globus_connect_server") ingest = _urn_scope(resource_server, "ingest") search = _urn_scope(resource_server, "search") + + +SearchScopes = _SearchScopes() diff --git a/src/globus_sdk/scopes/data/timers.py b/src/globus_sdk/scopes/data/timers.py index 75e506335..34f4e4acc 100644 --- a/src/globus_sdk/scopes/data/timers.py +++ b/src/globus_sdk/scopes/data/timers.py @@ -1,7 +1,10 @@ from ..collection import StaticScopeCollection, _url_scope -class TimersScopes(StaticScopeCollection): +class _TimersScopes(StaticScopeCollection): resource_server = "524230d7-ea86-4a52-8312-86065a9e0417" timer = _url_scope(resource_server, "timer") + + +TimersScopes = _TimersScopes() diff --git a/src/globus_sdk/scopes/data/transfer.py b/src/globus_sdk/scopes/data/transfer.py index 5d8c2288c..f6e23a43d 100644 --- a/src/globus_sdk/scopes/data/transfer.py +++ b/src/globus_sdk/scopes/data/transfer.py @@ -1,8 +1,11 @@ from ..collection import StaticScopeCollection, _urn_scope -class TransferScopes(StaticScopeCollection): +class _TransferScopes(StaticScopeCollection): resource_server = "transfer.api.globus.org" all = _urn_scope(resource_server, "all") gcp_install = _urn_scope(resource_server, "gcp_install") + + +TransferScopes = _TransferScopes() diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 7d5044233..c8bed093c 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -926,9 +926,7 @@ class SpecificFlowClient(client.BaseClient): error_class = FlowsAPIError service_name = "flows" - scopes: SpecificFlowScopes = ( - SpecificFlowScopes._CLASS_STUB # type: ignore[assignment] - ) + scopes: SpecificFlowScopes = SpecificFlowScopes._build_class_stub() def __init__( self, diff --git a/tests/functional/scopes/test_scope_data_behaviors.py b/tests/functional/scopes/test_scope_data_behaviors.py new file mode 100644 index 000000000..e3b96f26a --- /dev/null +++ b/tests/functional/scopes/test_scope_data_behaviors.py @@ -0,0 +1,64 @@ +import uuid + +import pytest + +from globus_sdk.scopes import ( + AuthScopes, + ComputeScopes, + FlowsScopes, + GCSCollectionScopes, + GCSEndpointScopes, + GroupsScopes, + NexusScopes, + Scope, + SearchScopes, + SpecificFlowScopes, + TimersScopes, + TransferScopes, +) + + +@pytest.mark.parametrize( + "collection, expect_resource_server", + ( + (AuthScopes, "auth.globus.org"), + (ComputeScopes, "funcx_service"), + (FlowsScopes, "flows.globus.org"), + (GroupsScopes, "groups.api.globus.org"), + (NexusScopes, "nexus.api.globus.org"), + (SearchScopes, "search.api.globus.org"), + (TimersScopes, "524230d7-ea86-4a52-8312-86065a9e0417"), + (TransferScopes, "transfer.api.globus.org"), + ), +) +def test_static_resource_server_attributes(collection, expect_resource_server): + assert collection.resource_server == expect_resource_server + + +@pytest.mark.parametrize( + "collection_cls", (GCSEndpointScopes, GCSCollectionScopes, SpecificFlowScopes) +) +def test_dynamic_resource_server_attributes(collection_cls): + some_id = str(uuid.UUID(int=1)) + coll = collection_cls(some_id) + assert coll.resource_server == some_id + + +def test_oidc_scope_formatting(): + assert str(AuthScopes.openid) == "openid" + assert str(AuthScopes.email) == "email" + assert str(AuthScopes.profile) == "profile" + + +def test_non_oidc_auth_scope_formatting(): + non_oidc_scopes = set(AuthScopes).difference( + (AuthScopes.openid, AuthScopes.email, AuthScopes.profile) + ) + + assert len(non_oidc_scopes) > 0 + assert all(isinstance(x, Scope) for x in non_oidc_scopes) + + scope_strs = [str(s) for s in non_oidc_scopes] + assert all( + s.startswith("urn:globus:auth:scope:auth.globus.org:") for s in scope_strs + ) diff --git a/tests/functional/services/gcs/test_scope_helpers.py b/tests/functional/services/gcs/test_scope_helpers.py index c0c532041..0441a0755 100644 --- a/tests/functional/services/gcs/test_scope_helpers.py +++ b/tests/functional/services/gcs/test_scope_helpers.py @@ -24,13 +24,9 @@ def test_data_access_scope_helper(client): assert not hasattr(sc, "manage_collections") -def test_str_contains_scope_properties(client): +def test_contains_scope_properties(client): ep_sc = client.get_gcs_endpoint_scopes(zero_id) - - assert "manage_collections" in str(ep_sc) - assert str(ep_sc.manage_collections) in str(ep_sc) + assert ep_sc.manage_collections in list(ep_sc) collection_sc = client.get_gcs_collection_scopes(zero_id) - - assert "data_access" in str(collection_sc) - assert str(collection_sc.data_access) in str(collection_sc) + assert collection_sc.data_access in list(collection_sc) diff --git a/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py b/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py index 32bb7aef3..f2cc07008 100644 --- a/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py +++ b/tests/non-pytest/mypy-ignore-tests/specific_flow_scopes.py @@ -8,8 +8,8 @@ specific_flow_client = globus_sdk.SpecificFlowClient(flow_id) scopes_object = specific_flow_client.scopes -t.assert_type(scopes_object, globus_sdk.scopes.DynamicScopeCollection) +t.assert_type(scopes_object, globus_sdk.scopes.SpecificFlowScopes) -scope: str = scopes_object.user +scope: globus_sdk.Scope = scopes_object.user x: int = scopes_object.user # type: ignore[assignment] resource_server: str = specific_flow_client.scopes.resource_server diff --git a/tests/unit/scopes/test_scope_collections.py b/tests/unit/scopes/test_scope_collections.py index fcc9423bd..784d1ed27 100644 --- a/tests/unit/scopes/test_scope_collections.py +++ b/tests/unit/scopes/test_scope_collections.py @@ -1,14 +1,12 @@ -import textwrap import uuid -from globus_sdk.scopes import ( - ComputeScopes, +from globus_sdk.scopes import ComputeScopes, FlowsScopes, Scope +from globus_sdk.scopes.collection import ( DynamicScopeCollection, - FlowsScopes, - Scope, StaticScopeCollection, + _url_scope, + _urn_scope, ) -from globus_sdk.scopes.collection import _url_scope, _urn_scope def test_url_scope_string(): @@ -28,17 +26,19 @@ def test_urn_scope_string(): assert str(s) == "urn:globus:auth:scope:example.globus.org:myscope" -def test_static_scope_collection_str_contains_expected_values(): - class MyScopes(StaticScopeCollection): +def test_static_scope_collection_iter_contains_expected_values(): + class _MyScopes(StaticScopeCollection): resource_server = str(uuid.UUID(int=0)) foo = _urn_scope(resource_server, "foo") bar = _url_scope(resource_server, "bar") - stringified = str(MyScopes) - assert MyScopes.resource_server in stringified - assert str(MyScopes.foo) in stringified - assert str(MyScopes.bar) in stringified + MyScopes = _MyScopes() + + listified = list(MyScopes) + as_set = set(listified) + assert len(listified) == len(as_set) + assert as_set == {MyScopes.foo, MyScopes.bar} def test_dynamic_scope_collection_contains_expected_values(): @@ -55,10 +55,11 @@ def bar(self): resource_server = str(uuid.UUID(int=10)) scope_collection = MyScopes(resource_server) - stringified = str(scope_collection) - assert scope_collection.resource_server in stringified - assert str(scope_collection.foo) in stringified - assert str(scope_collection.bar) in stringified + assert scope_collection.resource_server == resource_server + + listified = list(scope_collection) + assert scope_collection.foo in listified + assert scope_collection.bar in listified def test_flows_scopes_creation(): @@ -75,16 +76,3 @@ def test_compute_scopes_creation(): str(ComputeScopes.all) == "https://auth.globus.org/scopes/facd7ccc-c5f4-42aa-916b-a0e270e2c2a9/all" ) - - -def test_stringify_static_scope_collection(): - class MyScopes(StaticScopeCollection): - resource_server = "foo" - sc1 = _urn_scope(resource_server, "sc1") - - assert str(MyScopes) == textwrap.dedent( - """\ - MyScopes[foo] - sc1: - urn:globus:auth:scope:foo:sc1""" - ) From e44c7dd31bf03a1bbd004d9c127be2db6deff0f4 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 25 Jun 2025 12:41:32 -0500 Subject: [PATCH 072/176] Add documentation on ScopeCollection rewrite Changelog fragment + upgrading doc contents. --- ...3132_sirosen_service_scopes_collection.rst | 14 +++++ docs/upgrading.rst | 51 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 changelog.d/20250625_123132_sirosen_service_scopes_collection.rst diff --git a/changelog.d/20250625_123132_sirosen_service_scopes_collection.rst b/changelog.d/20250625_123132_sirosen_service_scopes_collection.rst new file mode 100644 index 000000000..03bfb40d9 --- /dev/null +++ b/changelog.d/20250625_123132_sirosen_service_scopes_collection.rst @@ -0,0 +1,14 @@ +Changed +------- + +- The ``ScopeBuilder`` types have been simplified and improved as the new + ``ScopeCollection`` types. (:pr:`NUMBER`) + + - ``ScopeBuilder`` is replaced with ``StaticScopeCollection`` and + ``DynamicScopeCollection``. The ``scopes`` attribute of client classes is + now a scope collection. + + - The attributes of ``ScopeCollection``\s are ``Scope`` objects, not strings. + + - ``ScopeCollection``\s define ``__iter__``, yielding the provided scopes, + but not ``__str__``. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7deccc133..b4e86c904 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -112,6 +112,45 @@ To control when a submission ID is fetched, use submission_id=submission_id, ) +Scope Constants Are Now Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Under version 3, many scopes were provided as string constants. +For example, ``globus_sdk.TransferClient.scopes.all`` was a string. + +In version 4, these constants are now :class:`Scope ` +objects. They can be rendered to strings using ``str()`` and no longer need to +be converted to :class:`Scope `\s in order to use +methods. + +Convert usage which stringifies scopes like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.scopes import AuthScopes + + my_scope_str: str = AuthScopes.openid + + # globus-sdk v4 + from globus_sdk.scopes import AuthScopes + + my_scope_str: str = str(AuthScopes.openid) + +And convert usage which builds scope objects like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.scopes import AuthScopes, Scope + + my_scope: Scope = Scope(AuthScopes.openid) + + # globus-sdk v4 + from globus_sdk.scopes import AuthScopes, Scope + + my_scope: Scope = AuthScopes.openid + Scopes Are Immutable and Have New Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -163,6 +202,18 @@ For example, update like so: my_scopes: list[Scope] = ScopeParser.parse(scope_string) +Scope Collections Provide ``__iter__``, not ``__str__`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In version 3, the SDK scope collection objects provided a pretty printer in the +form of ``str()``. Users could call ``str(TransferClient.scopes)`` to see the +available scopes. + +In version 4, this has been removed, but the collection types provide +``__iter__`` over their member scopes instead. Therefore, you can fetch all +scopes for the Globus Transfer service via ``list(TransferClient.scopes)`` or +similar usage. + Deprecated Timers Aliases Removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From f34f843bfbeeae5f55b0b05e1acf1333a906b687 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 25 Jun 2025 13:22:49 -0500 Subject: [PATCH 073/176] Split scope collection docs to improve doc flow Splitting these from the docs for `Scope` provides clearer focus to each page. --- .../scopes_and_consents/index.rst | 1 + .../scopes_and_consents/scope_collections.rst | 202 ++++++++++++++++ .../scopes_and_consents/scope_parsing.rst | 13 + .../scopes_and_consents/scopes.rst | 225 +----------------- 4 files changed, 225 insertions(+), 216 deletions(-) create mode 100644 docs/authorization/scopes_and_consents/scope_collections.rst diff --git a/docs/authorization/scopes_and_consents/index.rst b/docs/authorization/scopes_and_consents/index.rst index 0bbf07844..ee9dc53b7 100644 --- a/docs/authorization/scopes_and_consents/index.rst +++ b/docs/authorization/scopes_and_consents/index.rst @@ -30,5 +30,6 @@ which make learning about and manipulating these data easier. :maxdepth: 1 scopes + scope_collections consents scope_parsing diff --git a/docs/authorization/scopes_and_consents/scope_collections.rst b/docs/authorization/scopes_and_consents/scope_collections.rst new file mode 100644 index 000000000..57788c561 --- /dev/null +++ b/docs/authorization/scopes_and_consents/scope_collections.rst @@ -0,0 +1,202 @@ +.. _scope_collections: + +.. currentmodule:: globus_sdk.scopes + +ScopeCollections +================ + +OAuth2 Scopes for various Globus services are represented by ``ScopeCollection`` +objects. +These are containers for constant :class:`Scope` objects. + +Scope collections are provided directly via ``globus_sdk.scopes`` and are also +accessible via the relevant client classes. + +Direct Use +---------- + +To use the scope collections directly, import from ``globus_sdk.scopes``. + +For example, one might use the Transfer "all" scope during a login flow like +so: + +.. code-block:: python + + import globus_sdk + from globus_sdk.scopes import TransferScopes + + CLIENT_ID = "" + + client = globus_sdk.NativeAppAuthClient(CLIENT_ID) + client.oauth2_start_flow(requested_scopes=[TransferScopes.all]) + ... + +As Client Attributes +-------------------- + +Token scopes are associated with a particular client which will use that token. +Because of this, each service client contains a ``ScopeCollection`` attribute +(``client.scopes``) defining the relevant scopes for that client. + +For most client classes, this is a class attribute. For example, accessing +``TransferClient.scopes`` is valid: + +.. code-block:: python + + import globus_sdk + + CLIENT_ID = "" + + client = globus_sdk.NativeAppAuthClient(CLIENT_ID) + client.oauth2_start_flow(requested_scopes=[globus_sdk.TransferClient.scopes.all]) + ... + + # or, potentially, after there is a concrete client + tc = globus_sdk.TransferClient() + client.oauth2_start_flow(requested_scopes=[tc.scopes.all]) + +As Instance Attributes and Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some client classes only provide their scopes for instances. These cases cover +services which are distributed or contain multiple subservices with their own +scopes. + +For example, ``GCSClient`` and ``SpecificFlowClient`` each have a ``scopes`` +attribute of ``None`` on their classes. + +In the case of ``SpecificFlowClient``, scopes are populated whenever an +instance is instantiated. So the following usage is valid: + +.. code-block:: python + + import globus_sdk + + FLOW_ID = "" + + client = globus_sdk.SpecificFlowClient(FLOW_ID) + flow_user_scope = client.scopes.user + +In the case of GCS, a distributed service, ``scopes`` is always ``None``. +However, :meth:`globus_sdk.GCSClient.get_gcs_endpoint_scopes` and +:meth:`globus_sdk.GCSClient.get_gcs_collection_scopes` are available helpers +for getting specific collections of scopes. + +Using a Scope Collection to Get Matching Tokens +----------------------------------------------- + +A ``ScopeCollection`` contains the resource server name used to get token data +from a token response. +To elaborate on the above example: + +.. code-block:: python + + import globus_sdk + from globus_sdk.scopes import TransferScopes + + CLIENT_ID = "" + + client = globus_sdk.NativeAppAuthClient(CLIENT_ID) + client.oauth2_start_flow(requested_scopes=[TransferScopes.all]) + authorize_url = client.oauth2_get_authorize_url() + print("Please go to this URL and login:", authorize_url) + auth_code = input("Please enter the code you get after login here: ").strip() + token_response = client.oauth2_exchange_code_for_tokens(auth_code) + + # use the `resource_server` of a ScopeBuilder to grab the associated token + # data from the response + tokendata = token_response.by_resource_server[TransferScopes.resource_server] + + +Reference +--------- + +Collection Types +~~~~~~~~~~~~~~~~ + +.. autoclass:: ScopeCollection + :members: + :show-inheritance: + +.. autoclass:: StaticScopeCollection + :members: + :show-inheritance: + +.. autoclass:: DynamicScopeCollection + :members: + :show-inheritance: + +.. autoclass:: GCSEndpointScopes + :members: + :show-inheritance: + +.. autoclass:: GCSCollectionScopes + :members: + :show-inheritance: + +.. autoclass:: SpecificFlowScopes + :members: + :show-inheritance: + +Collection Constants +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: globus_sdk.scopes.data.AuthScopes + + Globus Auth scopes. + + .. listknownscopes:: globus_sdk.scopes.AuthScopes + :example_scope: view_identity_set + + +.. py:data:: globus_sdk.scopes.data.ComputeScopes + + Compute scopes. + + .. listknownscopes:: globus_sdk.scopes.ComputeScopes + + +.. py:data:: globus_sdk.scopes.data.FlowsScopes + + Globus Flows scopes. + + .. listknownscopes:: globus_sdk.scopes.FlowsScopes + + +.. py:data:: globus_sdk.scopes.data.GroupsScopes + + Groups scopes. + + .. listknownscopes:: globus_sdk.scopes.GroupsScopes + + +.. py:data:: globus_sdk.scopes.data.NexusScopes + + Nexus scopes. + + .. listknownscopes:: globus_sdk.scopes.NexusScopes + + .. warning:: + + Use of Nexus is deprecated. Users should use Groups instead. + + +.. py:data:: globus_sdk.scopes.data.SearchScopes + + Globus Search scopes. + + .. listknownscopes:: globus_sdk.scopes.SearchScopes + + +.. py:data:: globus_sdk.scopes.data.TimersScopes + + Globus Timers scopes. + + .. listknownscopes:: globus_sdk.scopes.TimersScopes + + +.. py:data:: globus_sdk.scopes.data.TransferScopes + + Globus Transfer scopes. + + .. listknownscopes:: globus_sdk.scopes.TransferScopes diff --git a/docs/authorization/scopes_and_consents/scope_parsing.rst b/docs/authorization/scopes_and_consents/scope_parsing.rst index f3dc91f8b..69710c085 100644 --- a/docs/authorization/scopes_and_consents/scope_parsing.rst +++ b/docs/authorization/scopes_and_consents/scope_parsing.rst @@ -20,3 +20,16 @@ ScopeParser Reference .. autoclass:: ScopeParser :members: :show-inheritance: + +.. autoclass:: ScopeParseError + +.. autoclass:: ScopeCycleError + +.. rubric:: Utility Functions + +``globus_sdk.scopes`` also provides helper functions which are used to +manipulate scope objects. + +.. autofunction:: scopes_to_str + +.. autofunction:: scopes_to_scope_list diff --git a/docs/authorization/scopes_and_consents/scopes.rst b/docs/authorization/scopes_and_consents/scopes.rst index 383537d22..b302372b2 100644 --- a/docs/authorization/scopes_and_consents/scopes.rst +++ b/docs/authorization/scopes_and_consents/scopes.rst @@ -2,111 +2,8 @@ .. currentmodule:: globus_sdk.scopes -Scopes and ScopeBuilders -======================== - -OAuth2 Scopes for various Globus services are represented by ``ScopeBuilder`` -objects. - -A number of preset scope builders are provided and populated with useful data, -and they are also accessible via the relevant client classes. - -Direct Use (As Constants) -------------------------- - -To use the scope builders directly, import from ``globus_sdk.scopes``. - -For example, one might use the Transfer "all" scope during a login flow like -so: - -.. code-block:: python - - import globus_sdk - from globus_sdk.scopes import TransferScopes - - CLIENT_ID = "" - - client = globus_sdk.NativeAppAuthClient(CLIENT_ID) - client.oauth2_start_flow(requested_scopes=[TransferScopes.all]) - ... - -As Client Attributes --------------------- - -Token scopes are associated with a particular client which will use that token. -Because of this, each service client contains a ``ScopeBuilder`` attribute (``client.scopes``) defining the relevant scopes for that client. - -For most client classes, this is a class attribute. For example, accessing -``TransferClient.scopes`` is valid: - -.. code-block:: python - - import globus_sdk - - CLIENT_ID = "" - - client = globus_sdk.NativeAppAuthClient(CLIENT_ID) - client.oauth2_start_flow(requested_scopes=[globus_sdk.TransferClient.scopes.all]) - ... - - # or, potentially, after there is a concrete client - _tc = globus_sdk.TransferClient() - client.oauth2_start_flow(requested_scopes=[_tc.scopes.all]) - -As Instance Attributes and Methods -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some client classes only provide their scopes for instances. These cases cover -services which are distributed or contain multiple subservices with their own -scopes. - -For example, ``GCSClient`` and ``SpecificFlowClient`` each have a ``scopes`` -attribute of ``None`` on their classes. - -In the case of ``SpecificFlowClient``, scopes are populated whenever an -instance is instantiated. So the following usage is valid: - -.. code-block:: python - - import globus_sdk - - FLOW_ID = "" - - client = globus_sdk.SpecificFlowClient(FLOW_ID) - flow_user_scope = client.scopes.user - -In the case of GCS, a distributed service, ``scopes`` is always ``None``. -However, :meth:`globus_sdk.GCSClient.get_gcs_endpoint_scopes` and -:meth:`globus_sdk.GCSClient.get_gcs_collection_scopes` are available helpers -for getting specific collections of scopes. - -Using a Scope Builder to Get Matching Tokens --------------------------------------------- - -A ``ScopeBuilder`` contains the resource server name used to get token data -from a token response. -To elaborate on the above example: - -.. code-block:: python - - import globus_sdk - from globus_sdk.scopes import TransferScopes - - CLIENT_ID = "" - - client = globus_sdk.NativeAppAuthClient(CLIENT_ID) - client.oauth2_start_flow(requested_scopes=[TransferScopes.all]) - authorize_url = client.oauth2_get_authorize_url() - print("Please go to this URL and login:", authorize_url) - auth_code = input("Please enter the code you get after login here: ").strip() - token_response = client.oauth2_exchange_code_for_tokens(auth_code) - - # use the `resource_server` of a ScopeBuilder to grab the associated token - # data from the response - tokendata = token_response.by_resource_server[TransferScopes.resource_server] - -Scope objects -------------- +Scopes +====== The SDK provides a ``Scope`` object which is the class model for a scope. ``Scope``\s can be parsed from strings and serialized to strings, and support @@ -129,11 +26,13 @@ For example, one can create a ``Scope`` object for the OIDC ``openid`` scope: * serializing (stringifying) * scope tree construction -Scope Construction -~~~~~~~~~~~~~~~~~~ +Tree Construction +~~~~~~~~~~~~~~~~~ ``Scope`` objects provide a tree-like interface for constructing scopes and their dependencies. +Because ``Scope`` objects are immutable, trees are constructed by building new +scopes. For example, the transfer scope dependent upon a collection scope may be constructed by means of ``Scope`` methods thusly: @@ -150,7 +49,7 @@ constructed by means of ``Scope`` methods thusly: transfer_scope = TransferScopes.all.with_dependency(data_access_scope, optional=True) ``Scope``\s can be used in most of the same locations where scope -strings can be used, but you can also call ``scope.serialize()`` to get a +strings can be used, but you can also call ``str(scope)`` to get a stringified representation. Serializing Scopes @@ -179,115 +78,9 @@ strings. All scope objects support this by means of their defined >>> print(repr(alpha)) Scope("alpha", dependencies=[Scope("beta", optional=True)]) -Scope Reference -~~~~~~~~~~~~~~~ +Reference +~~~~~~~~~ .. autoclass:: Scope :members: :member-order: bysource - -.. autoclass:: ScopeParseError - -.. autoclass:: ScopeCycleError - -.. rubric:: Utility Functions - -``globus_sdk.scopes`` also provides helper functions which are used to -manipulate scope objects. - -.. autofunction:: scopes_to_str - -.. autofunction:: scopes_to_scope_list - -ScopeBuilders -------------- - -Scope Collection Types -~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: ScopeCollection - :members: - :show-inheritance: - -.. autoclass:: StaticScopeCollection - :members: - :show-inheritance: - -.. autoclass:: DynamicScopeCollection - :members: - :show-inheritance: - -.. autoclass:: GCSEndpointScopes - :members: - :show-inheritance: - -.. autoclass:: GCSCollectionScopes - :members: - :show-inheritance: - -.. autoclass:: SpecificFlowScopes - :members: - :show-inheritance: - -ScopeCollection Constants -~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. py:data:: globus_sdk.scopes.data.AuthScopes - - Globus Auth scopes. - - .. listknownscopes:: globus_sdk.scopes.AuthScopes - :example_scope: view_identity_set - - -.. py:data:: globus_sdk.scopes.data.ComputeScopes - - Compute scopes. - - .. listknownscopes:: globus_sdk.scopes.ComputeScopes - - -.. py:data:: globus_sdk.scopes.data.FlowsScopes - - Globus Flows scopes. - - .. listknownscopes:: globus_sdk.scopes.FlowsScopes - - -.. py:data:: globus_sdk.scopes.data.GroupsScopes - - Groups scopes. - - .. listknownscopes:: globus_sdk.scopes.GroupsScopes - - -.. py:data:: globus_sdk.scopes.data.NexusScopes - - Nexus scopes. - - .. listknownscopes:: globus_sdk.scopes.NexusScopes - - .. warning:: - - Use of Nexus is deprecated. Users should use Groups instead. - - -.. py:data:: globus_sdk.scopes.data.SearchScopes - - Globus Search scopes. - - .. listknownscopes:: globus_sdk.scopes.SearchScopes - - -.. py:data:: globus_sdk.scopes.data.TimersScopes - - Globus Timers scopes. - - .. listknownscopes:: globus_sdk.scopes.TimersScopes - - -.. py:data:: globus_sdk.scopes.data.TransferScopes - - Globus Transfer scopes. - - .. listknownscopes:: globus_sdk.scopes.TransferScopes From af11122eeb065031b48992f3e2cfe858f3ee63e0 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 24 Jun 2025 10:14:07 -0500 Subject: [PATCH 074/176] Use 'is MISSING' to compare for missing-ness Add a TYPE_CHECKING -time definition of the MissingType and MISSING sentinel as an enum and enum member. This lets type checkers understand that an identity comparison against the MISSING sentinel is a valid way to type-narrow. Update the local flake8 plugin to scan for `isinstance(..., MissingType)` and treat that as an error. Update all of the usages flagged by this new lint. --- src/globus_sdk/_globus_sdk_flake8.py | 14 +++ src/globus_sdk/_missing.py | 89 ++++++++++++------- src/globus_sdk/_remarshal.py | 10 +-- .../services/auth/client/base_login_client.py | 2 +- .../auth/client/confidential_client.py | 6 +- .../services/auth/client/service_client.py | 12 +-- .../services/auth/flow_managers/native_app.py | 2 +- src/globus_sdk/services/flows/client.py | 6 +- .../services/gcs/data/collection.py | 8 +- src/globus_sdk/services/gcs/data/endpoint.py | 2 +- src/globus_sdk/services/search/client.py | 6 +- src/globus_sdk/services/search/data.py | 2 +- src/globus_sdk/services/transfer/client.py | 8 +- .../services/auth/test_auth_client_flow.py | 4 +- 14 files changed, 99 insertions(+), 72 deletions(-) diff --git a/src/globus_sdk/_globus_sdk_flake8.py b/src/globus_sdk/_globus_sdk_flake8.py index ae9635366..7c04dced5 100644 --- a/src/globus_sdk/_globus_sdk_flake8.py +++ b/src/globus_sdk/_globus_sdk_flake8.py @@ -9,6 +9,8 @@ # lexical scopes! "SDK001": "SDK001 loggers should be named 'log'", "SDK002": "SDK002 never use 'log.info'", + # don't do `isinstance(x, MissingType)` -- use `x is MISSING` instead + "SDK003": "SDK003 use `is MISSING`, not `isinstance(..., MissingType)`", } @@ -124,6 +126,18 @@ def visit_Assign(self, node: ast.Assign) -> None: # type_ignores=[]) # def visit_Call(self, node: ast.Call) -> None: + # check for `isinstance()` calls + if isinstance(node.func, ast.Name) and node.func.id == "isinstance": + if len(node.args) != 2: + self.generic_visit(node) + return + rhs = node.args[1] + if not isinstance(rhs, ast.Name): + self.generic_visit(node) + return + if rhs.id == "MissingType": + self._record(node, "SDK003") + # if it's not a call to 'x.info(...)', ignore if not (isinstance(node.func, ast.Attribute) and node.func.attr == "info"): self.generic_visit(node) diff --git a/src/globus_sdk/_missing.py b/src/globus_sdk/_missing.py index 94c96e629..f447377a4 100644 --- a/src/globus_sdk/_missing.py +++ b/src/globus_sdk/_missing.py @@ -10,40 +10,61 @@ T = t.TypeVar("T") - -class MissingType: - def __init__(self) -> None: - # disable instantiation, but gated to be able to run once - # when this module is imported - if "MISSING" in globals(): - raise TypeError("MissingType should not be instantiated") - - def __bool__(self) -> bool: - return False - - def __copy__(self) -> MissingType: - return self - - def __deepcopy__(self, memo: dict[int, t.Any]) -> MissingType: - return self - - # unpickling a MissingType should always return the "MISSING" sentinel - def __reduce__(self) -> str: - return "MISSING" - - def __repr__(self) -> str: - return "" - - -# a sentinel value for "missing" values which are distinguished from `None` (null) -# this is the default used to indicate that a parameter was not passed, so that -# method calls passing `None` can be distinguished from those which did not pass any -# value -# users should typically not use this value directly, but it is part of the public SDK -# interfaces along with its type for annotation purposes -# -# *new in version 3.30.0* -MISSING = MissingType() +if t.TYPE_CHECKING: + # pretend that `MISSING: MissingType` is an enum at type-checking time + # this allows type checkers to use identity comparisons to type narrow + # + # for example: + # + # x: int | float | MissingType + # if x is not MISSING: + # reveal_type(x) + # + # should show `x: int | float` + # + # however, because type checkers don't know that `MISSING` is a sentinel + # they do not narrow in this case based on the runtime data + import enum + + class _MissingEnum(enum.Enum): + MISSING = enum.auto() + + MissingType = _MissingEnum + MISSING = _MissingEnum.MISSING +else: + + class MissingType: + def __init__(self) -> None: + # disable instantiation, but gated to be able to run once + # when this module is imported + if "MISSING" in globals(): + raise TypeError("MissingType should not be instantiated") + + def __bool__(self) -> bool: + return False + + def __copy__(self) -> MissingType: + return self + + def __deepcopy__(self, memo: dict[int, t.Any]) -> MissingType: + return self + + # unpickling a MissingType should always return the "MISSING" sentinel + def __reduce__(self) -> str: + return "MISSING" + + def __repr__(self) -> str: + return "" + + # a sentinel value for "missing" values which are distinguished from `None` (null) + # this is the default used to indicate that a parameter was not passed, so that + # method calls passing `None` can be distinguished from those which did not pass any + # value + # users should typically not use this value directly, but it is part of the + # public SDK interfaces along with its type for annotation purposes + # + # *new in version 3.30.0* + MISSING = MissingType() @t.overload diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_remarshal.py index e35a43c8a..41fedbce3 100644 --- a/src/globus_sdk/_remarshal.py +++ b/src/globus_sdk/_remarshal.py @@ -40,7 +40,7 @@ def stringify(value: NullableOmittable[object]) -> NullableOmittable[str]: """ if value is None: return None - if isinstance(value, MissingType): + if value is MISSING: return MISSING return str(value) @@ -61,7 +61,7 @@ def listify(value: NullableOmittable[t.Iterable[T]]) -> NullableOmittable[list[T """ if value is None: return None - if isinstance(value, MissingType): + if value is MISSING: return MISSING if isinstance(value, list): return value @@ -131,7 +131,7 @@ def strseq_listify( """ if value is None: return None - if isinstance(value, MissingType): + if value is MISSING: return MISSING return list(strseq_iter(value)) @@ -161,7 +161,7 @@ def list_map( """ if value is None: return None - if isinstance(value, MissingType): + if value is MISSING: return MISSING return [mapped_function(element) for element in value] @@ -179,7 +179,7 @@ def commajoin( ) -> NullableOmittable[str]: if value is None: return None - if isinstance(value, MissingType): + if value is MISSING: return MISSING # note that this explicit handling of Iterable allows for objects to be # passed to this function and be stringified by the `str()` call diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 6e666eaec..0075c892e 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -134,7 +134,7 @@ def get_jwk( When not provided, it will be fetched automatically. :param as_pem: Decode the JWK to an RSA PEM key, typically for JWT decoding """ - if isinstance(openid_configuration, MissingType): + if openid_configuration is MISSING: log.debug("No OIDC Config provided, autofetching...") openid_configuration = self.get_openid_configuration() jwk_data = get_jwk_data( diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index c6fdb871a..7e966824c 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -265,11 +265,7 @@ def oauth2_get_dependent_tokens( # 'refresh_tokens' is consistent with the rest of the SDK and better # communicates expectations back to the user than the OAuth2 spec wording "access_type": "offline" if refresh_tokens else MISSING, - "scope": ( - " ".join(strseq_iter(scope)) - if not isinstance(scope, MissingType) - else scope - ), + "scope": (" ".join(strseq_iter(scope)) if scope is not MISSING else scope), **(additional_params or {}), } return self.oauth2_token(form_data, response_class=OAuthDependentTokenResponse) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 78ae68f44..4564f0a01 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -210,7 +210,7 @@ def get_jwk( :param as_pem: Decode the JWK to an RSA PEM key, typically for JWT decoding :type as_pem: bool """ - if isinstance(openid_configuration, MissingType): + if openid_configuration is MISSING: log.debug("No OIDC Config provided, autofetching...") openid_configuration = self.get_openid_configuration() jwk_data = get_jwk_data( @@ -1267,7 +1267,7 @@ def create_client( if terms_and_conditions or privacy_policy: body["links"] = links - if not isinstance(additional_fields, MissingType): + if additional_fields is not MISSING: body.update(additional_fields) return self.post("/v2/api/clients", data={"client": body}) @@ -1350,7 +1350,7 @@ def update_client( if terms_and_conditions is not MISSING or privacy_policy is not MISSING: body["links"] = links - if not isinstance(additional_fields, MissingType): + if additional_fields is not MISSING: body.update(additional_fields) return self.put(f"/v2/api/clients/{client_id}", data={"client": body}) @@ -1632,12 +1632,12 @@ def get_scopes( "'scopes_strings' and 'ids'. These are mutually exclusive." ) - if isinstance(query_params, MissingType): + if query_params is MISSING: query_params = {} - if not isinstance(scope_strings, MissingType): + if scope_strings is not MISSING: query_params["scope_strings"] = commajoin(scope_strings) - if not isinstance(ids, MissingType): + if ids is not MISSING: query_params["ids"] = commajoin(ids) return GetScopesResponse(self.get("/v2/api/scopes", query_params=query_params)) diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index 423deda30..3267acf43 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -153,7 +153,7 @@ def __init__( f"verifier=,challenge={self.challenge}" ) - if not isinstance(prefill_named_grant, MissingType): + if prefill_named_grant is not MISSING: log.debug(f"prefill_named_grant={self.prefill_named_grant}") def get_authorize_url(self, query_params: dict[str, t.Any] | None = None) -> str: diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 803b22cfa..696237b71 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -350,13 +350,11 @@ def list_flows( :service: flows :ref: Flows/paths/~1flows/get """ - if not isinstance(filter_role, MissingType): + if filter_role is not MISSING: exc.warn_deprecated( "The `filter_role` parameter is deprecated. Use `filter_roles` instead." ) - if not isinstance(filter_role, MissingType) and not isinstance( - filter_roles, MissingType - ): + if filter_role is not MISSING and filter_roles is not MISSING: msg = "Mutually exclusive parameters: filter_role and filter_roles." raise GlobusSDKUsageError(msg) query_params = { diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index 5018ed16d..dd34cdc92 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -207,7 +207,7 @@ def __init__( self["acl_expiration_mins"] = acl_expiration_mins self["associated_flow_policy"] = associated_flow_policy - if not isinstance(additional_fields, MissingType): + if additional_fields is not MISSING: self.update(additional_fields) @property @@ -522,7 +522,7 @@ def __init__( self["sharing_groups_allow"] = strseq_listify(sharing_groups_allow) self["sharing_groups_deny"] = strseq_listify(sharing_groups_deny) - if not isinstance(additional_fields, MissingType): + if additional_fields is not MISSING: self.update(additional_fields) @@ -553,7 +553,7 @@ def __init__( self["sharing_groups_allow"] = strseq_listify(sharing_groups_allow) self["sharing_groups_deny"] = strseq_listify(sharing_groups_deny) - if not isinstance(additional_fields, MissingType): + if additional_fields is not MISSING: self.update(additional_fields) @@ -577,5 +577,5 @@ def __init__( super().__init__() self["DATA_TYPE"] = DATA_TYPE self["project"] = project - if not isinstance(additional_fields, MissingType): + if additional_fields is not MISSING: self.update(additional_fields) diff --git a/src/globus_sdk/services/gcs/data/endpoint.py b/src/globus_sdk/services/gcs/data/endpoint.py index cde6b3b63..599d1c66e 100644 --- a/src/globus_sdk/services/gcs/data/endpoint.py +++ b/src/globus_sdk/services/gcs/data/endpoint.py @@ -137,6 +137,6 @@ def __init__( self["subscription_id"] = subscription_id self["gridftp_control_channel_port"] = gridftp_control_channel_port - if not isinstance(additional_fields, MissingType): + if additional_fields is not MISSING: self.update(additional_fields) ensure_datatype(self) diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index 864048dbd..0472f699e 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -358,9 +358,9 @@ def post_search( """ log.debug(f"SearchClient.post_search({index_id}, ...)") add_kwargs = {} - if not isinstance(offset, MissingType): + if offset is not MISSING: add_kwargs["offset"] = offset - if not isinstance(limit, MissingType): + if limit is not MISSING: add_kwargs["limit"] = limit data = {**data, **add_kwargs} return self.post(f"v1/index/{index_id}/search", data=data) @@ -409,7 +409,7 @@ def scroll( """ log.debug(f"SearchClient.scroll({index_id}, ...)") add_kwargs = {} - if not isinstance(marker, MissingType): + if marker is not MISSING: add_kwargs["marker"] = marker data = {**data, **add_kwargs} return self.post(f"v1/index/{index_id}/scroll", data=data) diff --git a/src/globus_sdk/services/search/data.py b/src/globus_sdk/services/search/data.py index 2a23636b7..d690f30ca 100644 --- a/src/globus_sdk/services/search/data.py +++ b/src/globus_sdk/services/search/data.py @@ -15,7 +15,7 @@ def _format_histogram_range( value: tuple[t.Any, t.Any] | MissingType, ) -> dict[str, t.Any] | MissingType: - if isinstance(value, MissingType): + if value is MISSING: return MISSING low, high = value return {"low": low, "high": high} diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 64ab05554..44c86be4d 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -28,7 +28,7 @@ def _datelike_to_str(x: DateLike) -> str: def _format_completion_time( x: str | tuple[DateLike, DateLike] | MissingType, ) -> str | MissingType: - if isinstance(x, MissingType): + if x is MISSING: return MISSING elif isinstance(x, str): return x @@ -47,7 +47,7 @@ def _format_filter_item(x: MissingType) -> MissingType: ... def _format_filter_item(x: str | TransferFilterDict | MissingType) -> str | MissingType: - if isinstance(x, MissingType): + if x is MISSING: return MISSING elif isinstance(x, str): return x @@ -2396,9 +2396,7 @@ def endpoint_manager_task_list( :ref: transfer/advanced_collection_management/#get_tasks """ # noqa: E501 log.debug("TransferClient.endpoint_manager_task_list(...)") - if isinstance(filter_endpoint, MissingType) and not isinstance( - filter_endpoint_use, MissingType - ): + if filter_endpoint is MISSING and filter_endpoint_use is not MISSING: raise exc.GlobusSDKUsageError( "`filter_endpoint_use` is only valid when `filter_endpoint` is " "also supplied." diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index 87cac2678..3455766eb 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -4,7 +4,7 @@ import pytest import globus_sdk -from globus_sdk._missing import MISSING, MissingType +from globus_sdk._missing import MISSING from globus_sdk._testing import load_response from globus_sdk.scopes import TransferScopes from globus_sdk.services.auth.flow_managers.native_app import _make_native_app_challenge @@ -131,7 +131,7 @@ def test_oauth2_get_authorize_url_supports_session_params( "session_required_single_domain" if domain_option else None, "session_required_identities" if identity_option else None, "session_required_policies" if policy_option else None, - "session_required_mfa" if not isinstance(mfa_option, MissingType) else None, + "session_required_mfa" if mfa_option is not MISSING else None, "prompt" if prompt_option else None, } expected_params_keys.discard(None) From da0a6e8ac7ef8c94ac9f958de3e76d29c6de76df Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 24 Jun 2025 11:10:42 -0500 Subject: [PATCH 075/176] Add a type checking test for MISSING This confirms that `MISSING` and `MissingType` behave appropriately at type checking time. Because `mypy` struggles with usages like `assert_type(x, MissingType)` when `MissingType` is defined as a type alias, simply remove the `_MissingEnum` placeholder and make the type of the enum `MissingType` for the type-checking definition. This is also a little shorter. --- src/globus_sdk/_missing.py | 5 ++-- .../mypy-ignore-tests/missing_type.py | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/non-pytest/mypy-ignore-tests/missing_type.py diff --git a/src/globus_sdk/_missing.py b/src/globus_sdk/_missing.py index f447377a4..a051a92a3 100644 --- a/src/globus_sdk/_missing.py +++ b/src/globus_sdk/_missing.py @@ -26,11 +26,10 @@ # they do not narrow in this case based on the runtime data import enum - class _MissingEnum(enum.Enum): + class MissingType(enum.Enum): MISSING = enum.auto() - MissingType = _MissingEnum - MISSING = _MissingEnum.MISSING + MISSING = MissingType.MISSING else: class MissingType: diff --git a/tests/non-pytest/mypy-ignore-tests/missing_type.py b/tests/non-pytest/mypy-ignore-tests/missing_type.py new file mode 100644 index 000000000..181e0d3cf --- /dev/null +++ b/tests/non-pytest/mypy-ignore-tests/missing_type.py @@ -0,0 +1,30 @@ +import typing as t + +from globus_sdk import MISSING, MissingType + +# first, the type of `MISSING` must be `MissingType` +t.assert_type(MISSING, MissingType) + +# second, a variable annotated as `int | MissingType` is assignable with `MISSING` +x: int | MissingType = MISSING + +# and `MissingType` is not the same as None, Ellipsis, False, 0, or other weirdness +# these error! +y: MissingType +y = None # type: ignore[assignment] +y = Ellipsis # type: ignore[assignment] +y = ... # type: ignore[assignment] +y = False # type: ignore[assignment] +y = 0 # type: ignore[assignment] + +# given that x is int|MissingType, `x is not MISSING` should narrow to `int` +if x is not MISSING: + t.assert_type(x, int) +else: + # don't do this: + # t.assert_type(x, MissingType) + # although that looks right, `MissingType` != `Literal[MISSING]`, so it fails + # (at least on some mypy versions) + # instead, confirm that `not isinstance(x, MissingType)` narrows to a Never + if not isinstance(x, MissingType): # noqa: SDK003 + t.assert_never(x) From dac9827f7c2e79f09192d0d9b899b46db9648b99 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 24 Jun 2025 11:24:46 -0500 Subject: [PATCH 076/176] Remove "new in" comment --- src/globus_sdk/_missing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/globus_sdk/_missing.py b/src/globus_sdk/_missing.py index a051a92a3..c31534f5f 100644 --- a/src/globus_sdk/_missing.py +++ b/src/globus_sdk/_missing.py @@ -61,8 +61,6 @@ def __repr__(self) -> str: # value # users should typically not use this value directly, but it is part of the # public SDK interfaces along with its type for annotation purposes - # - # *new in version 3.30.0* MISSING = MissingType() From 93273d10a3f20daf85644997af022ad1d4471312 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 24 Jun 2025 11:53:52 -0500 Subject: [PATCH 077/176] Reflow the new flake8 plugin rule Use a pipeline-like approach via assignment expressions to pass data from one step to the next, resulting in a coherent (large) compound conditional for each rule checked. The fallthrough case can then be the 'generic_visit' case, which improves legibility. Structured comments also help indicate each section. --- src/globus_sdk/_globus_sdk_flake8.py | 55 ++++++++++++++++------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/globus_sdk/_globus_sdk_flake8.py b/src/globus_sdk/_globus_sdk_flake8.py index 7c04dced5..452ea50ad 100644 --- a/src/globus_sdk/_globus_sdk_flake8.py +++ b/src/globus_sdk/_globus_sdk_flake8.py @@ -126,28 +126,35 @@ def visit_Assign(self, node: ast.Assign) -> None: # type_ignores=[]) # def visit_Call(self, node: ast.Call) -> None: - # check for `isinstance()` calls - if isinstance(node.func, ast.Name) and node.func.id == "isinstance": - if len(node.args) != 2: - self.generic_visit(node) - return - rhs = node.args[1] - if not isinstance(rhs, ast.Name): - self.generic_visit(node) - return - if rhs.id == "MissingType": - self._record(node, "SDK003") - - # if it's not a call to 'x.info(...)', ignore - if not (isinstance(node.func, ast.Attribute) and node.func.attr == "info"): - self.generic_visit(node) - return - func_node: ast.Attribute = node.func - - # if the function was not a method of something named "log", ignore - if not (isinstance(func_node.value, ast.Name) and func_node.value.id == "log"): + # +---------------------------------------------+ + # | check SDK003 | `isinstance(x, MissingType)` | + # +---------------------------------------------+ + if ( + # an `isinstance()` call with two arguments + # (really just means it's a valid call) + isinstance(node.func, ast.Name) + and node.func.id == "isinstance" + and len(args := node.args) == 2 + # where the second argument is a name node, + # not a tuple or other expression + and isinstance(rhs := args[1], ast.Name) + # and the name used is 'MissingType' + and rhs.id == "MissingType" + ): + self._record(node, "SDK003") + + # +--------------------------------+ + # | check SDK002 | `log.info(...)` | + # +--------------------------------+ + elif ( + # the function call is of the form 'OBJ.info(...)' + isinstance(func_node := node.func, ast.Attribute) + and func_node.attr == "info" + # and, more specifically, the object is named 'log', so + # it's `log.info(...) + and isinstance(func_node.value, ast.Name) + and func_node.value.id == "log" + ): + self._record(node, "SDK002") + else: self.generic_visit(node) - return - - # nothing left, it failed SDK002! - self._record(node, "SDK002") From 0f202f81f12c0a1ecc243821f172ebcfa13ff487 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 26 Jun 2025 23:04:53 -0500 Subject: [PATCH 078/176] Restructure flake8 plugin to be simpler to read For improved readability, each rule matching guard is described in a dedicated function, and the node visitor just calls those guards as appropriate. Comments with AST parse trees are removed to de-noise the file, since they don't have a good new logical home after the restructure (since one parse tree goes to multiple matchers). --- src/globus_sdk/_globus_sdk_flake8.py | 211 +++++++++++++-------------- 1 file changed, 102 insertions(+), 109 deletions(-) diff --git a/src/globus_sdk/_globus_sdk_flake8.py b/src/globus_sdk/_globus_sdk_flake8.py index 452ea50ad..f2fac7c9b 100644 --- a/src/globus_sdk/_globus_sdk_flake8.py +++ b/src/globus_sdk/_globus_sdk_flake8.py @@ -43,118 +43,111 @@ def __init__(self) -> None: def _record(self, node: ast.expr | ast.stmt, code: str) -> None: self.collect.append((node.lineno, node.col_offset, code)) - # see the structure of an assignment: - # - # >>> print(ast.dump(ast.parse("""\ - # ... log = logging.getLogger(__name__) - # ... """), indent=4)) - # - # Module( - # body=[ - # Assign( - # targets=[ - # Name(id='log', ctx=Store())], - # value=Call( - # func=Attribute( - # value=Name(id='logging', ctx=Load()), - # attr='getLogger', - # ctx=Load()), - # args=[ - # Name(id='__name__', ctx=Load())], - # keywords=[]))], - # type_ignores=[]) - # - # we will try to find an optimal path to bail out quickly on most assignments def visit_Assign(self, node: ast.Assign) -> None: - # the value must be a call and must be using attr access in the function call - # this eliminates bare funcs like `x = foo()` but allows `x = foo.bar()` - if not ( - isinstance(node.value, ast.Call) - and isinstance(node.value.func, ast.Attribute) - ): - self.generic_visit(node) - return - call_node: ast.Call = node.value - func_node: ast.Attribute = node.value.func - - # not getLogger? irrelevant! - # not a name access (e.g., `foo().bar`)? irrelevant! - # not `getLogger` of `logging` (e.g., `x.getLogger`)? irrelevant! (and weird!) - if not ( - func_node.attr == "getLogger" - and isinstance(func_node.value, ast.Name) - and func_node.value.id == "logging" - ): - self.generic_visit(node) - return - - # the assignee must be a single variable and it must be a name node - if not (len(node.targets) == 1 and isinstance(node.targets[0], ast.Name)): - self.generic_visit(node) - return - name_node: ast.Name = node.targets[0] - - # confirm that the `logging.getLogger` args look right, if not... ignore - if len(call_node.args) != 1 or not isinstance(call_node.args[0], ast.Name): - self.generic_visit(node) - return - logger_arg: ast.Name = call_node.args[0] - - # now, all data prepared, do the check: - # - if the argument to `getLogger` is `"__name__"` - # - and the assignee is not "log" - # that fails - # - # other usages are allowed, e.g. `liblog = logging.getLogger(otherlib_name) - if name_node.id != "log" and logger_arg.id == "__name__": + if matches_sdk001(node): self._record(node, "SDK001") - # see the structure of a call: - # - # >>> print(ast.dump(ast.parse("log.info('foo')"), indent=4)) - # Module( - # body=[ - # Expr( - # value=Call( - # func=Attribute( - # value=Name(id='log', ctx=Load()), - # attr='info', - # ctx=Load()), - # args=[ - # Constant(value='foo')], - # keywords=[]))], - # type_ignores=[]) - # + self.generic_visit(node) + def visit_Call(self, node: ast.Call) -> None: - # +---------------------------------------------+ - # | check SDK003 | `isinstance(x, MissingType)` | - # +---------------------------------------------+ - if ( - # an `isinstance()` call with two arguments - # (really just means it's a valid call) - isinstance(node.func, ast.Name) - and node.func.id == "isinstance" - and len(args := node.args) == 2 - # where the second argument is a name node, - # not a tuple or other expression - and isinstance(rhs := args[1], ast.Name) - # and the name used is 'MissingType' - and rhs.id == "MissingType" - ): + if matches_sdk003(node): self._record(node, "SDK003") - - # +--------------------------------+ - # | check SDK002 | `log.info(...)` | - # +--------------------------------+ - elif ( - # the function call is of the form 'OBJ.info(...)' - isinstance(func_node := node.func, ast.Attribute) - and func_node.attr == "info" - # and, more specifically, the object is named 'log', so - # it's `log.info(...) - and isinstance(func_node.value, ast.Name) - and func_node.value.id == "log" - ): + elif matches_sdk002(node): self._record(node, "SDK002") - else: - self.generic_visit(node) + + self.generic_visit(node) + + +def matches_sdk001(node: ast.Assign) -> bool: + """ + A matcher for the SDK001 lint rule. + + Checks for `x = logging.getLogger(__name__)` where `x` is not `log`. + + :param node: the assignment statement AST node to check + """ + # the value must be a call and must be using attr access in the function call + # this eliminates bare funcs like `x = foo()` but allows `x = foo.bar()` + if not ( + isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute) + ): + return False + + call_node: ast.Call = node.value + func_node: ast.Attribute = node.value.func + + # make sure it's 'logging.getLogger' and no other function + if not ( + func_node.attr == "getLogger" + and isinstance(func_node.value, ast.Name) + and func_node.value.id == "logging" + ): + return False + + # the assignee must be a single variable and it must be a name node + if not (len(node.targets) == 1 and isinstance(node.targets[0], ast.Name)): + return False + + # if the assigned name is `log`, then it cannot be a match + name_node: ast.Name = node.targets[0] + if name_node.id == "log": + return False + + # confirm that the `logging.getLogger` args are a single name -- that's the + # form it will be when `__name__` is passed + if len(call_node.args) != 1 or not isinstance(call_node.args[0], ast.Name): + return False + + # if the argument is some other name, e.g. `logging.getLogger(my_variable)`, + # then that cannot be a match + logger_arg: ast.Name = call_node.args[0] + if logger_arg.id != "__name__": + return False + + return True # all conditions met! + + +def matches_sdk002(node: ast.Call) -> bool: + """ + A matcher for the SDK002 lint rule. + + Checks for `log.info(...)` + + :param node: the function call AST node to check + """ + func_node = node.func + # the function call must be of the form 'OBJ.info(...)' + if not (isinstance(func_node, ast.Attribute) and func_node.attr == "info"): + return False + + # and, more specifically, the object must be named 'log', so + # it's `log.info(...) + if not (isinstance(func_node.value, ast.Name) and func_node.value.id == "log"): + return False + + return True # all conditions met! + + +def matches_sdk003(node: ast.Call) -> bool: + """ + A matcher for the SDK003 lint rule. + + Checks for `isinstance(x, MissingType)` + + :param node: the function call AST node to check + """ + # it must be a call to a function named "isinstance" + if not (isinstance(node.func, ast.Name) and node.func.id == "isinstance"): + return False + # if the number of arguments is improper, it can't be a violation (not a + # valid 'isinstance' call) + if len(node.args) != 2: + return False + right_hand_side = node.args[1] + # the second argument must be the name 'MissingType' + if not ( + isinstance(right_hand_side, ast.Name) and right_hand_side.id == "MissingType" + ): + return False + + return True # all conditions met! From a070af02168b653d7f3c198f1eee9317ed89f36f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 26 Jun 2025 23:12:07 -0500 Subject: [PATCH 079/176] Improve comments on MISSING type checker trick --- src/globus_sdk/_missing.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/globus_sdk/_missing.py b/src/globus_sdk/_missing.py index c31534f5f..d30ac9b80 100644 --- a/src/globus_sdk/_missing.py +++ b/src/globus_sdk/_missing.py @@ -10,20 +10,29 @@ T = t.TypeVar("T") +# type checkers don't know that MISSING is a sentinel, so we will describe it +# differently at typing time, allowing for type narrowing on `is MISSING` and +# similar checks if t.TYPE_CHECKING: # pretend that `MISSING: MissingType` is an enum at type-checking time - # this allows type checkers to use identity comparisons to type narrow # - # for example: + # enums are treated as `Literal[...]` values and are narrowed under simple + # checks, as unions and literal types are + # therefore, under this definition, `MissingType ~= Literal[MissingType.MISSING]` + # + # Therefore, consider this example: # # x: int | float | MissingType # if x is not MISSING: # reveal_type(x) # - # should show `x: int | float` + # This is effectively the same as if we wrote: + # + # x: int | float | Literal["a"] + # if x != "a": + # reveal_type(x) # - # however, because type checkers don't know that `MISSING` is a sentinel - # they do not narrow in this case based on the runtime data + # Both should show `x: int | float` import enum class MissingType(enum.Enum): From 322c5ac1730c42797a39c24fb978ebe64fef0c0a Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 1 Jul 2025 16:17:06 +0000 Subject: [PATCH 080/176] (actions) update PR references --- ...0605_091936_sirosen_remove_deprecated_scope_parser_alias.rst | 2 +- .../20250620_104615_sirosen_remove_transfer_client_param.rst | 2 +- .../20250623_074800_kurtmckee_fix_changelog_rendering.rst | 2 +- changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst b/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst index 78369f338..a667af94e 100644 --- a/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst +++ b/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst @@ -2,4 +2,4 @@ Removed ------- - ``globus_sdk.experimental.scope_parser`` has been removed. Use - ``globus_sdk.scopes`` instead. (:pr:`NUMBER`) + ``globus_sdk.scopes`` instead. (:pr:`1236`) diff --git a/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst b/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst index 3c9ba1177..34f23b1a4 100644 --- a/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst +++ b/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst @@ -2,4 +2,4 @@ Breaking Changes ---------------- - The ``transfer_client`` parameter to ``TransferData`` and ``DeleteData`` has been removed. - See the upgrading doc for transition details. (:pr:`NUMBER`) + See the upgrading doc for transition details. (:pr:`1236`) diff --git a/changelog.d/20250623_074800_kurtmckee_fix_changelog_rendering.rst b/changelog.d/20250623_074800_kurtmckee_fix_changelog_rendering.rst index 10ed7110a..708df9f94 100644 --- a/changelog.d/20250623_074800_kurtmckee_fix_changelog_rendering.rst +++ b/changelog.d/20250623_074800_kurtmckee_fix_changelog_rendering.rst @@ -1,7 +1,7 @@ Development ----------- -- Convert the CHANGELOG to Markdown-compatible headers. (:pr:`NUMBER`) +- Convert the CHANGELOG to Markdown-compatible headers. (:pr:`1236`) This resolves rendering issues in Dependabot PRs in the CLI, and simplifies compatibility between RST and Markdown. diff --git a/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst b/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst index 2968ca9ba..690e66c13 100644 --- a/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst +++ b/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst @@ -2,4 +2,4 @@ Breaking Changes ---------------- - In Globus Auth client classes, defaults of ``None`` are converted to - ``MISSING`` for optional fields. (:pr:`NUMBER`) + ``MISSING`` for optional fields. (:pr:`1236`) From f34013609071eedc30bca609b77a25f225b398a1 Mon Sep 17 00:00:00 2001 From: m1yag1 <8730430+m1yag1@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:48:02 -0500 Subject: [PATCH 081/176] Fix compute client imports for 4.x-dev branch structure --- src/globus_sdk/services/compute/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index 4f83ecfc3..03247f1f8 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -3,7 +3,9 @@ import logging import typing as t -from globus_sdk import MISSING, GlobusHTTPResponse, MissingType, client, utils +from globus_sdk import GlobusHTTPResponse, client +from globus_sdk._missing import MISSING, MissingType +from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.scopes import ComputeScopes, Scope From fe50b55ca7c143eb7f0c85460216f0bcc1cc8271 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:08:28 +0000 Subject: [PATCH 082/176] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/globus_sdk/globus_app/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index eaacba67a..150f0aa56 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -385,7 +385,9 @@ def _auth_params_with_required_scopes( # merge scopes for deduplication to minimize url request length # this is useful even if there weren't any auth_param scope requirements # as the app's scope_requirements can have duplicates - combined_scopes = ScopeParser.merge_scopes(required_scopes, parsed_required_scopes) + combined_scopes = ScopeParser.merge_scopes( + required_scopes, parsed_required_scopes + ) auth_params.required_scopes = [str(s) for s in combined_scopes] return auth_params From 3926f60d1029c4c53a1a636b88328300dca5516a Mon Sep 17 00:00:00 2001 From: m1yag1 <8730430+m1yag1@users.noreply.github.com> Date: Thu, 10 Jul 2025 13:37:08 -0500 Subject: [PATCH 083/176] Bump version and changelog for release --- ...specific_flow_data_access_scope_helper.rst | 7 -- ...0603_181734_sirosen_split_scope_parser.rst | 35 ------- ...n_remove_deprecated_scope_parser_alias.rst | 5 - ...cke_sc_15807_transfer_missing_defaults.rst | 4 - .../20250611_012221_sirosen_utils_cleanup.rst | 8 -- ...5_sirosen_remove_transfer_client_param.rst | 5 - ...3_112759_sirosen_auth_missing_defaults.rst | 5 - ...1204_sirosen_service_scopes_collection.rst | 13 --- ...3132_sirosen_service_scopes_collection.rst | 14 --- changelog.rst | 95 +++++++++++++++++++ pyproject.toml | 2 +- 11 files changed, 96 insertions(+), 97 deletions(-) delete mode 100644 changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst delete mode 100644 changelog.d/20250603_181734_sirosen_split_scope_parser.rst delete mode 100644 changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst delete mode 100644 changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst delete mode 100644 changelog.d/20250611_012221_sirosen_utils_cleanup.rst delete mode 100644 changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst delete mode 100644 changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst delete mode 100644 changelog.d/20250624_011204_sirosen_service_scopes_collection.rst delete mode 100644 changelog.d/20250625_123132_sirosen_service_scopes_collection.rst diff --git a/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst b/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst deleted file mode 100644 index 20f439e07..000000000 --- a/changelog.d/20250415_155555_sirosen_add_specific_flow_data_access_scope_helper.rst +++ /dev/null @@ -1,7 +0,0 @@ -Added ------ - -- ``SpecificFlowClient`` has a new method, - ``add_app_transfer_data_access_scope`` which facilitates declaration of scope - requirements when starting flows which interact with collections that need - ``data_access`` scopes. (:pr:`1166`) diff --git a/changelog.d/20250603_181734_sirosen_split_scope_parser.rst b/changelog.d/20250603_181734_sirosen_split_scope_parser.rst deleted file mode 100644 index 5b3032cbd..000000000 --- a/changelog.d/20250603_181734_sirosen_split_scope_parser.rst +++ /dev/null @@ -1,35 +0,0 @@ -Changed -------- - -- ``Scope`` objects are now immutable. (:pr:`1208`) - - - ``Scope.dependencies`` is now a tuple, not a list. - - - The ``add_dependency`` method has been removed, since mutating a ``Scope`` - is no longer possible. - - - A new evolver method, ``Scope.with_dependency`` has been added. It extends - the ``dependencies`` tuple in a new ``Scope`` object. - - - A batch version of ``Scope.with_dependency`` has been added, - ``Scope.with_dependencies``. - - - An evolver for the ``optional`` field of a ``Scope`` is also now available, - named ``Scope.with_optional``. - -- Scope parsing has been separated from the main ``Scope`` class into a - dedicated ``ScopeParser`` which provides parsing methods. (:pr:`1208`) - - - Use ``globus_sdk.scopes.ScopeParser`` for complex parsing use-cases. The - ``ScopeParser.parse`` classmethod parses strings into lists of scope - objects. - - - ``Scope.merge_scopes`` has been moved to ``ScopeParser.merge_scopes``. - - - ``Scope.parse`` is changed to call ``ScopeParser.parse`` and verify that - there is exactly one result, which it returns. This means that - ``Scope.parse`` now returns a single ``Scope``, not a ``list[Scope]``. - - - ``Scope.serialize`` and ``Scope.deserialize`` have been removed as methods. - Use ``str(scope_object)`` as a replacement for ``serialize()`` and - ``Scope.parse`` as a replacement for ``deserialize()``. diff --git a/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst b/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst deleted file mode 100644 index a667af94e..000000000 --- a/changelog.d/20250605_091936_sirosen_remove_deprecated_scope_parser_alias.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -------- - -- ``globus_sdk.experimental.scope_parser`` has been removed. Use - ``globus_sdk.scopes`` instead. (:pr:`1236`) diff --git a/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst b/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst deleted file mode 100644 index f27b81ad4..000000000 --- a/changelog.d/20250605_142657_max.tuecke_sc_15807_transfer_missing_defaults.rst +++ /dev/null @@ -1,4 +0,0 @@ -Breaking Changes ----------------- - -- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Transfer client. (:pr:`1216`) diff --git a/changelog.d/20250611_012221_sirosen_utils_cleanup.rst b/changelog.d/20250611_012221_sirosen_utils_cleanup.rst deleted file mode 100644 index f036d583b..000000000 --- a/changelog.d/20250611_012221_sirosen_utils_cleanup.rst +++ /dev/null @@ -1,8 +0,0 @@ -Changed -------- - -- Payload types now inherit from ``dict`` rather than ``UserDict``. The - ``PayloadWrapper`` utility class has been replaced with ``Payload``. - (:pr:`1222`) -- Payload types are more consistent about encoding missing values using ``MISSING``. - (:pr:`1222`) diff --git a/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst b/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst deleted file mode 100644 index 34f23b1a4..000000000 --- a/changelog.d/20250620_104615_sirosen_remove_transfer_client_param.rst +++ /dev/null @@ -1,5 +0,0 @@ -Breaking Changes ----------------- - -- The ``transfer_client`` parameter to ``TransferData`` and ``DeleteData`` has been removed. - See the upgrading doc for transition details. (:pr:`1236`) diff --git a/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst b/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst deleted file mode 100644 index 690e66c13..000000000 --- a/changelog.d/20250623_112759_sirosen_auth_missing_defaults.rst +++ /dev/null @@ -1,5 +0,0 @@ -Breaking Changes ----------------- - -- In Globus Auth client classes, defaults of ``None`` are converted to - ``MISSING`` for optional fields. (:pr:`1236`) diff --git a/changelog.d/20250624_011204_sirosen_service_scopes_collection.rst b/changelog.d/20250624_011204_sirosen_service_scopes_collection.rst deleted file mode 100644 index d8d65c5bc..000000000 --- a/changelog.d/20250624_011204_sirosen_service_scopes_collection.rst +++ /dev/null @@ -1,13 +0,0 @@ -Changed -------- - -- The SDK's ``ScopeBuilder`` types have been replaced with - ``StaticScopeCollection`` and ``DynamicScopeCollection`` types. (:pr:`NUMBER`) - - - Scopes provided as constants by the SDK are now ``Scope`` objects, not - strings. They can be converted to strings trivially with ``str(scope)``. - - - The various scope builder types have been renamed. ``SpecificFlowScopes``, - ``GCSEndpointScopes``, and ``GCSCollectionScopes`` replace - ``SpecificFlowScopeBuilder``, ``GCSEndpointScopeBuilder``, and - ``GCSCollectionScopeBuilder``. diff --git a/changelog.d/20250625_123132_sirosen_service_scopes_collection.rst b/changelog.d/20250625_123132_sirosen_service_scopes_collection.rst deleted file mode 100644 index 03bfb40d9..000000000 --- a/changelog.d/20250625_123132_sirosen_service_scopes_collection.rst +++ /dev/null @@ -1,14 +0,0 @@ -Changed -------- - -- The ``ScopeBuilder`` types have been simplified and improved as the new - ``ScopeCollection`` types. (:pr:`NUMBER`) - - - ``ScopeBuilder`` is replaced with ``StaticScopeCollection`` and - ``DynamicScopeCollection``. The ``scopes`` attribute of client classes is - now a scope collection. - - - The attributes of ``ScopeCollection``\s are ``Scope`` objects, not strings. - - - ``ScopeCollection``\s define ``__iter__``, yielding the provided scopes, - but not ``__str__``. diff --git a/changelog.rst b/changelog.rst index b8ab4af6c..2c38c002b 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,101 @@ to a major new version of the SDK. .. scriv-insert-here +.. _changelog-4.0.0a3: + +v4.0.0a3 (2025-07-10) +===================== + +Breaking Changes +---------------- + +- All defaults of ``None`` converted to ``globus_sdk.MISSING`` for all payload types in the Transfer client. (:pr:`1216`) + +- The ``transfer_client`` parameter to ``TransferData`` and ``DeleteData`` has been removed. + See the upgrading doc for transition details. (:pr:`1236`) + +- In Globus Auth client classes, defaults of ``None`` are converted to + ``MISSING`` for optional fields. (:pr:`1236`) + +Added +----- + +- ``SpecificFlowClient`` has a new method, + ``add_app_transfer_data_access_scope`` which facilitates declaration of scope + requirements when starting flows which interact with collections that need + ``data_access`` scopes. (:pr:`1166`) + +Removed +------- + +- ``globus_sdk.experimental.scope_parser`` has been removed. Use + ``globus_sdk.scopes`` instead. (:pr:`1236`) + +Changed +------- + +- ``Scope`` objects are now immutable. (:pr:`1208`) + + - ``Scope.dependencies`` is now a tuple, not a list. + + - The ``add_dependency`` method has been removed, since mutating a ``Scope`` + is no longer possible. + + - A new evolver method, ``Scope.with_dependency`` has been added. It extends + the ``dependencies`` tuple in a new ``Scope`` object. + + - A batch version of ``Scope.with_dependency`` has been added, + ``Scope.with_dependencies``. + + - An evolver for the ``optional`` field of a ``Scope`` is also now available, + named ``Scope.with_optional``. + +- Scope parsing has been separated from the main ``Scope`` class into a + dedicated ``ScopeParser`` which provides parsing methods. (:pr:`1208`) + + - Use ``globus_sdk.scopes.ScopeParser`` for complex parsing use-cases. The + ``ScopeParser.parse`` classmethod parses strings into lists of scope + objects. + + - ``Scope.merge_scopes`` has been moved to ``ScopeParser.merge_scopes``. + + - ``Scope.parse`` is changed to call ``ScopeParser.parse`` and verify that + there is exactly one result, which it returns. This means that + ``Scope.parse`` now returns a single ``Scope``, not a ``list[Scope]``. + + - ``Scope.serialize`` and ``Scope.deserialize`` have been removed as methods. + Use ``str(scope_object)`` as a replacement for ``serialize()`` and + ``Scope.parse`` as a replacement for ``deserialize()``. + +- Payload types now inherit from ``dict`` rather than ``UserDict``. The + ``PayloadWrapper`` utility class has been replaced with ``Payload``. + (:pr:`1222`) +- Payload types are more consistent about encoding missing values using ``MISSING``. + (:pr:`1222`) + +- The SDK's ``ScopeBuilder`` types have been replaced with + ``StaticScopeCollection`` and ``DynamicScopeCollection`` types. (:pr:`NUMBER`) + + - Scopes provided as constants by the SDK are now ``Scope`` objects, not + strings. They can be converted to strings trivially with ``str(scope)``. + + - The various scope builder types have been renamed. ``SpecificFlowScopes``, + ``GCSEndpointScopes``, and ``GCSCollectionScopes`` replace + ``SpecificFlowScopeBuilder``, ``GCSEndpointScopeBuilder``, and + ``GCSCollectionScopeBuilder``. + +- The ``ScopeBuilder`` types have been simplified and improved as the new + ``ScopeCollection`` types. (:pr:`NUMBER`) + + - ``ScopeBuilder`` is replaced with ``StaticScopeCollection`` and + ``DynamicScopeCollection``. The ``scopes`` attribute of client classes is + now a scope collection. + + - The attributes of ``ScopeCollection``\s are ``Scope`` objects, not strings. + + - ``ScopeCollection``\s define ``__iter__``, yielding the provided scopes, + but not ``__str__``. + .. _changelog-4.0.0a2: v4.0.0a2 (2025-06-05) diff --git a/pyproject.toml b/pyproject.toml index 35bbe2f9e..93ae978d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "globus-sdk" -version = "4.0.0a2" +version = "4.0.0a3" authors = [ { name = "Globus Team", email = "support@globus.org" }, ] From 00d7e16447f5a7057ec4056d4c65fe63a32a68e7 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 10 Jul 2025 16:40:59 -0500 Subject: [PATCH 084/176] Rename `tokenstorage` to `token_storage` (#1252) * Renamed tokenstorage * Updated usage sites in docs * Updated usage sites in src * Updated usage sites in tests * Renamed docs files/folders using tokenstorage * Added changelog * Requested change: removed experimental.tokenstorage * Requested change: add experimental.tokenstorage to list of removed aliases --- ...ax.tuecke_sc_35529_rename_tokenstorage.rst | 4 ++ docs/authorization/token_caching/index.rst | 2 +- .../token_caching/storage_adapters.rst | 10 ++--- .../token_caching/token_storages.rst | 2 +- .../list_and_create_projects.py | 2 +- .../auth_manage_projects/manage_projects.py | 2 +- .../create_and_run_flow/manage_flow.py | 2 +- .../manage_flow_minimal.py | 2 +- .../create_and_run_flow/run_flow_minimal.py | 2 +- docs/examples/group_listing.rst | 4 +- docs/examples/index.rst | 2 +- .../dynamodb_token_storage.py} | 2 +- .../{tokenstorage => token_storage}/index.rst | 6 +-- docs/upgrading.rst | 1 + .../list_groups_noapp_with_storage.py | 2 +- .../getting_started/minimal_script_noapp.rst | 2 +- src/globus_sdk/experimental/tokenstorage.py | 43 ------------------- src/globus_sdk/globus_app/app.py | 2 +- .../globus_app/authorizer_factory.py | 4 +- src/globus_sdk/globus_app/config.py | 10 ++--- src/globus_sdk/globus_app/protocols.py | 2 +- src/globus_sdk/globus_app/user_app.py | 2 +- .../__init__.py | 0 .../v1/__init__.py | 0 .../v1/base.py | 0 .../v1/file_adapters.py | 4 +- .../v1/memory_adapter.py | 0 .../v1/sqlite_adapter.py | 0 .../v2/__init__.py | 0 .../v2/base.py | 0 .../v2/json.py | 0 .../v2/memory.py | 0 .../v2/sqlite.py | 0 .../v2/token_data.py | 0 .../v2/validating_token_storage/__init__.py | 0 .../v2/validating_token_storage/context.py | 0 .../v2/validating_token_storage/errors.py | 0 .../v2/validating_token_storage/storage.py | 0 .../v2/validating_token_storage/validators.py | 0 .../test_globus_app_token_handling.py | 10 ++--- .../tokenstorage/v1/test_simplejson_file.py | 2 +- .../functional/tokenstorage/v1/test_sqlite.py | 2 +- tests/functional/tokenstorage/v2/conftest.py | 2 +- .../v2/test_common_tokenstorage.py | 2 +- .../tokenstorage/v2/test_json_tokenstorage.py | 2 +- .../v2/test_memory_tokenstorage.py | 2 +- .../v2/test_sqlite_tokenstorage.py | 2 +- .../test_modules_do_not_require_requests.py | 2 +- .../unit/experimental/test_legacy_support.py | 9 ---- .../globus_app/test_authorizer_factory.py | 4 +- .../globus_app/test_client_integration.py | 2 +- tests/unit/globus_app/test_globus_app.py | 2 +- tests/unit/test_base_client.py | 2 +- .../tokenstorage/v1/test_memory_adapter.py | 2 +- .../v1/test_simplejson_adapter.py | 2 +- .../tokenstorage/v1/test_sqlite_adapter.py | 2 +- .../tokenstorage/v2/test_token_storage.py | 2 +- .../v2/test_validating_token_storage.py | 4 +- 58 files changed, 62 insertions(+), 109 deletions(-) create mode 100644 changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst rename docs/examples/{tokenstorage/dynamodb_tokenstorage.py => token_storage/dynamodb_token_storage.py} (99%) rename docs/examples/{tokenstorage => token_storage}/index.rst (79%) delete mode 100644 src/globus_sdk/experimental/tokenstorage.py rename src/globus_sdk/{tokenstorage => token_storage}/__init__.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v1/__init__.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v1/base.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v1/file_adapters.py (99%) rename src/globus_sdk/{tokenstorage => token_storage}/v1/memory_adapter.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v1/sqlite_adapter.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/__init__.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/base.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/json.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/memory.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/sqlite.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/token_data.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/validating_token_storage/__init__.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/validating_token_storage/context.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/validating_token_storage/errors.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/validating_token_storage/storage.py (100%) rename src/globus_sdk/{tokenstorage => token_storage}/v2/validating_token_storage/validators.py (100%) diff --git a/changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst b/changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst new file mode 100644 index 000000000..c5a666568 --- /dev/null +++ b/changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst @@ -0,0 +1,4 @@ +Changed +------- + +- Renamed the ``globus_sdk.tokenstorage`` subpackage to ``globus_sdk.token_storage`` and removed the ``globus_sdk.experimental.tokenstorage`` (:pr:`1252`) diff --git a/docs/authorization/token_caching/index.rst b/docs/authorization/token_caching/index.rst index 0c03a2abe..c9a36a53a 100644 --- a/docs/authorization/token_caching/index.rst +++ b/docs/authorization/token_caching/index.rst @@ -9,7 +9,7 @@ recommend using the former. ``TokenStorage`` is a newer iteration of the token s interface and includes a superset of the functionality previously supported in ``StorageAdapter``. -All constructs from both hierarchies are importable from the ``globus_sdk.tokenstorage`` +All constructs from both hierarchies are importable from the ``globus_sdk.token_storage`` namespace. .. toctree:: diff --git a/docs/authorization/token_caching/storage_adapters.rst b/docs/authorization/token_caching/storage_adapters.rst index 164a8d9ea..03ccf244e 100644 --- a/docs/authorization/token_caching/storage_adapters.rst +++ b/docs/authorization/token_caching/storage_adapters.rst @@ -15,7 +15,7 @@ received from authentication and token refreshes. Usage ----- -StorageAdapter is available under the name ``globus_sdk.tokenstorage``. +StorageAdapter is available under the name ``globus_sdk.token_storage``. Storage adapters are the main objects of this subpackage. Primarily, usage should revolve around creating a storage adapter, potentially loading data from @@ -27,7 +27,7 @@ For example: import os import globus_sdk - from globus_sdk.tokenstorage import SimpleJSONFileAdapter + from globus_sdk.token_storage import SimpleJSONFileAdapter my_file_adapter = SimpleJSONFileAdapter(os.path.expanduser("~/mytokens.json")) @@ -80,15 +80,15 @@ Complete Example Usage ~~~~~~~~~~~~~~~~~~~~~~ The :ref:`Group Listing With Token Storage Script ` -provides a complete and runnable example which leverages ``tokenstorage``. +provides a complete and runnable example which leverages ``token_storage``. Adapter Types ------------- -.. module:: globus_sdk.tokenstorage +.. module:: globus_sdk.token_storage -``globus_sdk.tokenstorage`` provides base classes for building your own storage +``globus_sdk.token_storage`` provides base classes for building your own storage adapters, and several complete adapters. The :class:`SimpleJSONFileAdapter` is good for the "simplest possible" diff --git a/docs/authorization/token_caching/token_storages.rst b/docs/authorization/token_caching/token_storages.rst index 5c127bba4..eec710c30 100644 --- a/docs/authorization/token_caching/token_storages.rst +++ b/docs/authorization/token_caching/token_storages.rst @@ -1,6 +1,6 @@ .. _token_storages: -.. currentmodule:: globus_sdk.tokenstorage +.. currentmodule:: globus_sdk.token_storage Token Storages ============== diff --git a/docs/examples/auth_manage_projects/list_and_create_projects.py b/docs/examples/auth_manage_projects/list_and_create_projects.py index 50d44eda1..24ed32ef4 100644 --- a/docs/examples/auth_manage_projects/list_and_create_projects.py +++ b/docs/examples/auth_manage_projects/list_and_create_projects.py @@ -4,7 +4,7 @@ import os import globus_sdk -from globus_sdk.tokenstorage import SimpleJSONFileAdapter +from globus_sdk.token_storage import SimpleJSONFileAdapter MY_FILE_ADAPTER = SimpleJSONFileAdapter( os.path.expanduser("~/.sdk-manage-projects.json") diff --git a/docs/examples/auth_manage_projects/manage_projects.py b/docs/examples/auth_manage_projects/manage_projects.py index 535c56383..02dc128fd 100644 --- a/docs/examples/auth_manage_projects/manage_projects.py +++ b/docs/examples/auth_manage_projects/manage_projects.py @@ -4,7 +4,7 @@ import os import globus_sdk -from globus_sdk.tokenstorage import SimpleJSONFileAdapter +from globus_sdk.token_storage import SimpleJSONFileAdapter MY_FILE_ADAPTER = SimpleJSONFileAdapter( os.path.expanduser("~/.sdk-manage-projects.json") diff --git a/docs/examples/create_and_run_flow/manage_flow.py b/docs/examples/create_and_run_flow/manage_flow.py index d96a22d9a..1275757f1 100644 --- a/docs/examples/create_and_run_flow/manage_flow.py +++ b/docs/examples/create_and_run_flow/manage_flow.py @@ -4,7 +4,7 @@ import sys import globus_sdk -from globus_sdk.tokenstorage import SimpleJSONFileAdapter +from globus_sdk.token_storage import SimpleJSONFileAdapter MY_FILE_ADAPTER = SimpleJSONFileAdapter(os.path.expanduser("~/.sdk-manage-flow.json")) diff --git a/docs/examples/create_and_run_flow/manage_flow_minimal.py b/docs/examples/create_and_run_flow/manage_flow_minimal.py index 75f97232c..e574a50bc 100644 --- a/docs/examples/create_and_run_flow/manage_flow_minimal.py +++ b/docs/examples/create_and_run_flow/manage_flow_minimal.py @@ -5,7 +5,7 @@ import sys import globus_sdk -from globus_sdk.tokenstorage import SimpleJSONFileAdapter +from globus_sdk.token_storage import SimpleJSONFileAdapter MY_FILE_ADAPTER = SimpleJSONFileAdapter(os.path.expanduser("~/.sdk-manage-flow.json")) diff --git a/docs/examples/create_and_run_flow/run_flow_minimal.py b/docs/examples/create_and_run_flow/run_flow_minimal.py index 60e79c598..e6dbbb2a7 100644 --- a/docs/examples/create_and_run_flow/run_flow_minimal.py +++ b/docs/examples/create_and_run_flow/run_flow_minimal.py @@ -5,7 +5,7 @@ import sys import globus_sdk -from globus_sdk.tokenstorage import SimpleJSONFileAdapter +from globus_sdk.token_storage import SimpleJSONFileAdapter MY_FILE_ADAPTER = SimpleJSONFileAdapter(os.path.expanduser("~/.sdk-manage-flow.json")) diff --git a/docs/examples/group_listing.rst b/docs/examples/group_listing.rst index a46f3448b..e218b6286 100644 --- a/docs/examples/group_listing.rst +++ b/docs/examples/group_listing.rst @@ -62,7 +62,7 @@ For simplicity, the script will prompt for login on each use. Group Listing With Token Storage -------------------------------- -``globus_sdk.tokenstorage`` provides tools for managing refresh tokens. The +``globus_sdk.token_storage`` provides tools for managing refresh tokens. The following example script shows how you might use this to provide a complete script which lists the current user's groups using refresh tokens. @@ -72,7 +72,7 @@ script which lists the current user's groups using refresh tokens. import os from globus_sdk import GroupsClient, NativeAppAuthClient, RefreshTokenAuthorizer - from globus_sdk.tokenstorage import SimpleJSONFileAdapter + from globus_sdk.token_storage import SimpleJSONFileAdapter CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" AUTH_CLIENT = NativeAppAuthClient(CLIENT_ID) diff --git a/docs/examples/index.rst b/docs/examples/index.rst index bfea118e2..8b0b07e2d 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -9,7 +9,7 @@ Each of these pages contains an example of a piece of SDK functionality. minimal_transfer_script/index auth_manage_projects/index create_and_run_flow/index - tokenstorage/index + token_storage/index group_listing authorization native_app diff --git a/docs/examples/tokenstorage/dynamodb_tokenstorage.py b/docs/examples/token_storage/dynamodb_token_storage.py similarity index 99% rename from docs/examples/tokenstorage/dynamodb_tokenstorage.py rename to docs/examples/token_storage/dynamodb_token_storage.py index f37198dad..552b3bc28 100644 --- a/docs/examples/tokenstorage/dynamodb_tokenstorage.py +++ b/docs/examples/token_storage/dynamodb_token_storage.py @@ -7,7 +7,7 @@ import boto3 import globus_sdk -from globus_sdk.tokenstorage import StorageAdapter +from globus_sdk.token_storage import StorageAdapter CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" tablename = "example-globus-tokenstorage" diff --git a/docs/examples/tokenstorage/index.rst b/docs/examples/token_storage/index.rst similarity index 79% rename from docs/examples/tokenstorage/index.rst rename to docs/examples/token_storage/index.rst index dadafaefe..ad23630df 100644 --- a/docs/examples/tokenstorage/index.rst +++ b/docs/examples/token_storage/index.rst @@ -1,4 +1,4 @@ -.. _example_tokenstorage: +.. _example_token_storage: Token Storage Adapters ====================== @@ -16,6 +16,6 @@ sequential scans for enumeration. The example therefore demonstrates that key-value stores with limited or no capabilities for table scans can be used to implement the token storage interface. -.. literalinclude:: dynamodb_tokenstorage.py - :caption: ``dynamodb_tokenstorage.py`` [:download:`download `] +.. literalinclude:: dynamodb_token_storage.py + :caption: ``dynamodb_token_storage.py`` [:download:`download `] :language: python diff --git a/docs/upgrading.rst b/docs/upgrading.rst index b4e86c904..7be36710f 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -239,6 +239,7 @@ The removed alias and new module names are shown in the table below. "``globus_sdk.experimental.auth_requirements_error``", "``globus_sdk.gare``" "``globus_sdk.experimental.scope_parser``", "``globus_sdk.scopes``" + "``globus_sdk.experimental.tokenstorage``", "``globus_sdk.token_storage``" ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/user_guide/getting_started/list_groups_noapp_with_storage.py b/docs/user_guide/getting_started/list_groups_noapp_with_storage.py index 4425fe96e..8156e4d8a 100644 --- a/docs/user_guide/getting_started/list_groups_noapp_with_storage.py +++ b/docs/user_guide/getting_started/list_groups_noapp_with_storage.py @@ -1,7 +1,7 @@ import os import globus_sdk -from globus_sdk.tokenstorage import JSONTokenStorage +from globus_sdk.token_storage import JSONTokenStorage # this is the tutorial client ID # replace this string with your ID for production use diff --git a/docs/user_guide/getting_started/minimal_script_noapp.rst b/docs/user_guide/getting_started/minimal_script_noapp.rst index ef8d9758c..088d08373 100644 --- a/docs/user_guide/getting_started/minimal_script_noapp.rst +++ b/docs/user_guide/getting_started/minimal_script_noapp.rst @@ -182,7 +182,7 @@ Defining a token storage object is simple: .. code-block:: python import os - from globus_sdk.tokenstorage import JSONTokenStorage + from globus_sdk.token_storage import JSONTokenStorage token_storage = JSONTokenStorage( os.path.expanduser("~/.list-my-globus-groups-tokens.json") diff --git a/src/globus_sdk/experimental/tokenstorage.py b/src/globus_sdk/experimental/tokenstorage.py deleted file mode 100644 index 51b0a11ce..000000000 --- a/src/globus_sdk/experimental/tokenstorage.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import sys -import typing as t - -__all__ = ( - "JSONTokenStorage", - "SQLiteTokenStorage", - "TokenStorage", - "FileTokenStorage", - "MemoryTokenStorage", - "TokenStorageData", -) - -# legacy aliases -# (when accessed, these will emit deprecation warnings) -if t.TYPE_CHECKING: - from globus_sdk.tokenstorage import ( - FileTokenStorage, - JSONTokenStorage, - MemoryTokenStorage, - SQLiteTokenStorage, - TokenStorage, - TokenStorageData, - ) -else: - - def __getattr__(name: str) -> t.Any: - import globus_sdk.tokenstorage as tokenstorage_module - from globus_sdk.exc import warn_deprecated - - warn_deprecated( - "'globus_sdk.experimental.tokenstorage' has been renamed to " - "'globus_sdk.tokenstorage'. " - f"Importing '{name}' from `globus_sdk.experimental` is deprecated. " - f"Use `globus_sdk.tokenstorage.{name}` instead." - ) - - value = getattr(tokenstorage_module, name, None) - if value is None: - raise AttributeError(f"module {__name__} has no attribute {name}") - setattr(sys.modules[__name__], name, value) - return value diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index 150f0aa56..374c7c901 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -15,7 +15,7 @@ from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.scopes import AuthScopes, Scope, ScopeParser, scopes_to_scope_list -from globus_sdk.tokenstorage import ( +from globus_sdk.token_storage import ( ScopeRequirementsValidator, TokenStorage, TokenValidationError, diff --git a/src/globus_sdk/globus_app/authorizer_factory.py b/src/globus_sdk/globus_app/authorizer_factory.py index 173a69614..349505777 100644 --- a/src/globus_sdk/globus_app/authorizer_factory.py +++ b/src/globus_sdk/globus_app/authorizer_factory.py @@ -12,8 +12,8 @@ RefreshTokenAuthorizer, ) from globus_sdk.services.auth import OAuthTokenResponse -from globus_sdk.tokenstorage import ValidatingTokenStorage -from globus_sdk.tokenstorage.v2.validating_token_storage import MissingTokenError +from globus_sdk.token_storage import ValidatingTokenStorage +from globus_sdk.token_storage.v2.validating_token_storage import MissingTokenError GA = t.TypeVar("GA", bound=GlobusAuthorizer) diff --git a/src/globus_sdk/globus_app/config.py b/src/globus_sdk/globus_app/config.py index 778013150..22f01f3db 100644 --- a/src/globus_sdk/globus_app/config.py +++ b/src/globus_sdk/globus_app/config.py @@ -10,14 +10,14 @@ LocalServerLoginFlowManager, LoginFlowManager, ) -from globus_sdk.tokenstorage import ( +from globus_sdk.token_storage import ( JSONTokenStorage, MemoryTokenStorage, SQLiteTokenStorage, TokenStorage, TokenValidationError, ) -from globus_sdk.tokenstorage.v2.validating_token_storage import IdentityMismatchError +from globus_sdk.token_storage.v2.validating_token_storage import IdentityMismatchError from .protocols import ( IDTokenDecoderProvider, @@ -71,9 +71,9 @@ class GlobusAppConfig: :ivar str | ``TokenStorage`` | ``TokenStorageProvider`` token_storage: A class responsible for storing and retrieving tokens. This may be either a well-known provider (one of - :class:`"json" `, - :class:`"sqlite" `, or - :class:`"memory" `) or a custom + :class:`"json" `, + :class:`"sqlite" `, or + :class:`"memory" `) or a custom storage/provider. Default: ``"json"``. :ivar str | ``LoginFlowManager`` | ``LoginFlowManagerProvider`` login_flow_manager: diff --git a/src/globus_sdk/globus_app/protocols.py b/src/globus_sdk/globus_app/protocols.py index f7379af52..02f0e3d1f 100644 --- a/src/globus_sdk/globus_app/protocols.py +++ b/src/globus_sdk/globus_app/protocols.py @@ -6,7 +6,7 @@ from globus_sdk import AuthLoginClient, IDTokenDecoder from globus_sdk._types import UUIDLike from globus_sdk.login_flows import LoginFlowManager - from globus_sdk.tokenstorage import TokenStorage, TokenValidationError + from globus_sdk.token_storage import TokenStorage, TokenValidationError from .app import GlobusApp from .config import GlobusAppConfig diff --git a/src/globus_sdk/globus_app/user_app.py b/src/globus_sdk/globus_app/user_app.py index aef011214..6346971f3 100644 --- a/src/globus_sdk/globus_app/user_app.py +++ b/src/globus_sdk/globus_app/user_app.py @@ -13,7 +13,7 @@ from globus_sdk._types import ScopeCollectionType, UUIDLike from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows import CommandLineLoginFlowManager, LoginFlowManager -from globus_sdk.tokenstorage import ( +from globus_sdk.token_storage import ( HasRefreshTokensValidator, NotExpiredValidator, TokenStorage, diff --git a/src/globus_sdk/tokenstorage/__init__.py b/src/globus_sdk/token_storage/__init__.py similarity index 100% rename from src/globus_sdk/tokenstorage/__init__.py rename to src/globus_sdk/token_storage/__init__.py diff --git a/src/globus_sdk/tokenstorage/v1/__init__.py b/src/globus_sdk/token_storage/v1/__init__.py similarity index 100% rename from src/globus_sdk/tokenstorage/v1/__init__.py rename to src/globus_sdk/token_storage/v1/__init__.py diff --git a/src/globus_sdk/tokenstorage/v1/base.py b/src/globus_sdk/token_storage/v1/base.py similarity index 100% rename from src/globus_sdk/tokenstorage/v1/base.py rename to src/globus_sdk/token_storage/v1/base.py diff --git a/src/globus_sdk/tokenstorage/v1/file_adapters.py b/src/globus_sdk/token_storage/v1/file_adapters.py similarity index 99% rename from src/globus_sdk/tokenstorage/v1/file_adapters.py rename to src/globus_sdk/token_storage/v1/file_adapters.py index f5655a2f6..f0113dfd1 100644 --- a/src/globus_sdk/tokenstorage/v1/file_adapters.py +++ b/src/globus_sdk/token_storage/v1/file_adapters.py @@ -55,7 +55,7 @@ def _raw_load(self) -> dict[str, t.Any]: return val def _handle_formats(self, read_data: dict[str, t.Any]) -> _JSONFileData: - """Handle older data formats supported by globus_sdk.tokenstorage + """Handle older data formats supported by globus_sdk.token_storage if the data is not in a known/recognized format, this will error otherwise, reshape the data to the current supported format and return it @@ -110,7 +110,7 @@ def store(self, token_response: globus_sdk.OAuthTokenResponse) -> None: Given a token response, extract all the token data and write it to ``self.filename`` as JSON data. - Additionally will write the version of ``globus_sdk.tokenstorage`` + Additionally will write the version of ``globus_sdk.token_storage`` which was in use. Under the assumption that this may be running on a system with multiple diff --git a/src/globus_sdk/tokenstorage/v1/memory_adapter.py b/src/globus_sdk/token_storage/v1/memory_adapter.py similarity index 100% rename from src/globus_sdk/tokenstorage/v1/memory_adapter.py rename to src/globus_sdk/token_storage/v1/memory_adapter.py diff --git a/src/globus_sdk/tokenstorage/v1/sqlite_adapter.py b/src/globus_sdk/token_storage/v1/sqlite_adapter.py similarity index 100% rename from src/globus_sdk/tokenstorage/v1/sqlite_adapter.py rename to src/globus_sdk/token_storage/v1/sqlite_adapter.py diff --git a/src/globus_sdk/tokenstorage/v2/__init__.py b/src/globus_sdk/token_storage/v2/__init__.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/__init__.py rename to src/globus_sdk/token_storage/v2/__init__.py diff --git a/src/globus_sdk/tokenstorage/v2/base.py b/src/globus_sdk/token_storage/v2/base.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/base.py rename to src/globus_sdk/token_storage/v2/base.py diff --git a/src/globus_sdk/tokenstorage/v2/json.py b/src/globus_sdk/token_storage/v2/json.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/json.py rename to src/globus_sdk/token_storage/v2/json.py diff --git a/src/globus_sdk/tokenstorage/v2/memory.py b/src/globus_sdk/token_storage/v2/memory.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/memory.py rename to src/globus_sdk/token_storage/v2/memory.py diff --git a/src/globus_sdk/tokenstorage/v2/sqlite.py b/src/globus_sdk/token_storage/v2/sqlite.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/sqlite.py rename to src/globus_sdk/token_storage/v2/sqlite.py diff --git a/src/globus_sdk/tokenstorage/v2/token_data.py b/src/globus_sdk/token_storage/v2/token_data.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/token_data.py rename to src/globus_sdk/token_storage/v2/token_data.py diff --git a/src/globus_sdk/tokenstorage/v2/validating_token_storage/__init__.py b/src/globus_sdk/token_storage/v2/validating_token_storage/__init__.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/validating_token_storage/__init__.py rename to src/globus_sdk/token_storage/v2/validating_token_storage/__init__.py diff --git a/src/globus_sdk/tokenstorage/v2/validating_token_storage/context.py b/src/globus_sdk/token_storage/v2/validating_token_storage/context.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/validating_token_storage/context.py rename to src/globus_sdk/token_storage/v2/validating_token_storage/context.py diff --git a/src/globus_sdk/tokenstorage/v2/validating_token_storage/errors.py b/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/validating_token_storage/errors.py rename to src/globus_sdk/token_storage/v2/validating_token_storage/errors.py diff --git a/src/globus_sdk/tokenstorage/v2/validating_token_storage/storage.py b/src/globus_sdk/token_storage/v2/validating_token_storage/storage.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/validating_token_storage/storage.py rename to src/globus_sdk/token_storage/v2/validating_token_storage/storage.py diff --git a/src/globus_sdk/tokenstorage/v2/validating_token_storage/validators.py b/src/globus_sdk/token_storage/v2/validating_token_storage/validators.py similarity index 100% rename from src/globus_sdk/tokenstorage/v2/validating_token_storage/validators.py rename to src/globus_sdk/token_storage/v2/validating_token_storage/validators.py diff --git a/tests/functional/globus_app/test_globus_app_token_handling.py b/tests/functional/globus_app/test_globus_app_token_handling.py index ab5b979f6..ecf7e8810 100644 --- a/tests/functional/globus_app/test_globus_app_token_handling.py +++ b/tests/functional/globus_app/test_globus_app_token_handling.py @@ -4,7 +4,7 @@ import responses import globus_sdk -import globus_sdk.tokenstorage +import globus_sdk.token_storage from globus_sdk._testing import RegisteredResponse, load_response # the JWT will have a client ID in its audience claim @@ -124,7 +124,7 @@ def _count_jwk_calls(): calls = [c for c in calls if c.url == "https://auth.globus.org/jwk.json"] return len(calls) - memory_storage = globus_sdk.tokenstorage.MemoryTokenStorage() + memory_storage = globus_sdk.token_storage.MemoryTokenStorage() config = globus_sdk.GlobusAppConfig( token_storage=memory_storage, id_token_decoder=InfiniteLeewayDecoder, @@ -164,7 +164,7 @@ def __init__(self, *args, **kwargs) -> None: nonlocal init_counter init_counter += 1 - memory_storage = globus_sdk.tokenstorage.MemoryTokenStorage() + memory_storage = globus_sdk.token_storage.MemoryTokenStorage() config = globus_sdk.GlobusAppConfig( token_storage=memory_storage, id_token_decoder=CustomDecoder, @@ -195,7 +195,7 @@ def decode(self, *args, **kwargs) -> None: return super().decode(*args, **kwargs) login_client = globus_sdk.NativeAppAuthClient(client_id=CLIENT_ID_FROM_JWT) - memory_storage = globus_sdk.tokenstorage.MemoryTokenStorage() + memory_storage = globus_sdk.token_storage.MemoryTokenStorage() config = globus_sdk.GlobusAppConfig( token_storage=memory_storage, id_token_decoder=CustomDecoder(login_client) ) @@ -222,7 +222,7 @@ def test_globus_app_custom_id_token_decoder_instance_can_overload_jwt_leeway( load_response(globus_sdk.NativeAppAuthClient.oauth2_revoke_token) login_client = globus_sdk.NativeAppAuthClient(client_id=CLIENT_ID_FROM_JWT) - memory_storage = globus_sdk.tokenstorage.MemoryTokenStorage() + memory_storage = globus_sdk.token_storage.MemoryTokenStorage() config = globus_sdk.GlobusAppConfig( token_storage=memory_storage, id_token_decoder=globus_sdk.IDTokenDecoder( diff --git a/tests/functional/tokenstorage/v1/test_simplejson_file.py b/tests/functional/tokenstorage/v1/test_simplejson_file.py index ed6153ac4..371784f11 100644 --- a/tests/functional/tokenstorage/v1/test_simplejson_file.py +++ b/tests/functional/tokenstorage/v1/test_simplejson_file.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import __version__ -from globus_sdk.tokenstorage import SimpleJSONFileAdapter +from globus_sdk.token_storage import SimpleJSONFileAdapter IS_WINDOWS = os.name == "nt" diff --git a/tests/functional/tokenstorage/v1/test_sqlite.py b/tests/functional/tokenstorage/v1/test_sqlite.py index 176a4d3ac..95361b5cf 100644 --- a/tests/functional/tokenstorage/v1/test_sqlite.py +++ b/tests/functional/tokenstorage/v1/test_sqlite.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk.tokenstorage import SQLiteAdapter +from globus_sdk.token_storage import SQLiteAdapter @pytest.fixture diff --git a/tests/functional/tokenstorage/v2/conftest.py b/tests/functional/tokenstorage/v2/conftest.py index 2bbe171e0..f00823c35 100644 --- a/tests/functional/tokenstorage/v2/conftest.py +++ b/tests/functional/tokenstorage/v2/conftest.py @@ -6,7 +6,7 @@ import globus_sdk from globus_sdk._testing import RegisteredResponse -from globus_sdk.tokenstorage import TokenStorageData +from globus_sdk.token_storage import TokenStorageData @pytest.fixture diff --git a/tests/functional/tokenstorage/v2/test_common_tokenstorage.py b/tests/functional/tokenstorage/v2/test_common_tokenstorage.py index 1162b20f3..f732710df 100644 --- a/tests/functional/tokenstorage/v2/test_common_tokenstorage.py +++ b/tests/functional/tokenstorage/v2/test_common_tokenstorage.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk.tokenstorage import ( +from globus_sdk.token_storage import ( JSONTokenStorage, MemoryTokenStorage, SQLiteTokenStorage, diff --git a/tests/functional/tokenstorage/v2/test_json_tokenstorage.py b/tests/functional/tokenstorage/v2/test_json_tokenstorage.py index 04221130e..b383cf862 100644 --- a/tests/functional/tokenstorage/v2/test_json_tokenstorage.py +++ b/tests/functional/tokenstorage/v2/test_json_tokenstorage.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import __version__ -from globus_sdk.tokenstorage import JSONTokenStorage, SimpleJSONFileAdapter +from globus_sdk.token_storage import JSONTokenStorage, SimpleJSONFileAdapter IS_WINDOWS = os.name == "nt" diff --git a/tests/functional/tokenstorage/v2/test_memory_tokenstorage.py b/tests/functional/tokenstorage/v2/test_memory_tokenstorage.py index 9783e0b7b..f2631144a 100644 --- a/tests/functional/tokenstorage/v2/test_memory_tokenstorage.py +++ b/tests/functional/tokenstorage/v2/test_memory_tokenstorage.py @@ -1,4 +1,4 @@ -from globus_sdk.tokenstorage import MemoryTokenStorage +from globus_sdk.token_storage import MemoryTokenStorage def test_store_and_get_token_data_by_resource_server( diff --git a/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py b/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py index 5662afc57..552f3a2d1 100644 --- a/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py +++ b/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import exc -from globus_sdk.tokenstorage import SQLiteAdapter, SQLiteTokenStorage +from globus_sdk.token_storage import SQLiteAdapter, SQLiteTokenStorage @pytest.fixture diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index d7a97dc2b..8bd4faabe 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -28,7 +28,7 @@ "paging", "response", "scopes", - "tokenstorage", + "token_storage", # the top-level of the 'exc' subpackage (but not necessarily its contents) # should similarly be standalone, for exception handlers "exc", diff --git a/tests/unit/experimental/test_legacy_support.py b/tests/unit/experimental/test_legacy_support.py index 16f9825fe..349f8c1a1 100644 --- a/tests/unit/experimental/test_legacy_support.py +++ b/tests/unit/experimental/test_legacy_support.py @@ -23,15 +23,6 @@ def test_login_flow_manager_importable_from_experimental(): ) -def test_tokenstorage_importable_from_experimental(): - with pytest.warns(RemovedInV4Warning): - from globus_sdk.experimental.tokenstorage import ( # noqa: F401 - JSONTokenStorage, - MemoryTokenStorage, - SQLiteTokenStorage, - ) - - def test_globus_app_importable_from_experimental(): # This construct should be imported from `globus_sdk.globus_app`. with pytest.warns(RemovedInV4Warning, match=r"globus_sdk\.globus_app\."): diff --git a/tests/unit/globus_app/test_authorizer_factory.py b/tests/unit/globus_app/test_authorizer_factory.py index 111ce963e..cb38521d9 100644 --- a/tests/unit/globus_app/test_authorizer_factory.py +++ b/tests/unit/globus_app/test_authorizer_factory.py @@ -8,12 +8,12 @@ ClientCredentialsAuthorizerFactory, RefreshTokenAuthorizerFactory, ) -from globus_sdk.tokenstorage import ( +from globus_sdk.token_storage import ( HasRefreshTokensValidator, MemoryTokenStorage, NotExpiredValidator, ) -from globus_sdk.tokenstorage.v2.validating_token_storage import ( +from globus_sdk.token_storage.v2.validating_token_storage import ( ExpiredTokenError, MissingTokenError, ValidatingTokenStorage, diff --git a/tests/unit/globus_app/test_client_integration.py b/tests/unit/globus_app/test_client_integration.py index 126761f8d..e676eb5ec 100644 --- a/tests/unit/globus_app/test_client_integration.py +++ b/tests/unit/globus_app/test_client_integration.py @@ -5,7 +5,7 @@ import globus_sdk from globus_sdk import GlobusApp, GlobusAppConfig, UserApp from globus_sdk._testing import load_response -from globus_sdk.tokenstorage import MemoryTokenStorage +from globus_sdk.token_storage import MemoryTokenStorage @pytest.fixture diff --git a/tests/unit/globus_app/test_globus_app.py b/tests/unit/globus_app/test_globus_app.py index 13cd38529..7149871ac 100644 --- a/tests/unit/globus_app/test_globus_app.py +++ b/tests/unit/globus_app/test_globus_app.py @@ -33,7 +33,7 @@ LoginFlowManager, ) from globus_sdk.scopes import AuthScopes, Scope -from globus_sdk.tokenstorage import ( +from globus_sdk.token_storage import ( HasRefreshTokensValidator, JSONTokenStorage, MemoryTokenStorage, diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index e6555262e..0b08f689c 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -10,7 +10,7 @@ from globus_sdk._testing import RegisteredResponse, get_last_request from globus_sdk.authorizers import NullAuthorizer from globus_sdk.scopes import Scope, TransferScopes -from globus_sdk.tokenstorage import TokenValidationError +from globus_sdk.token_storage import TokenValidationError @pytest.fixture diff --git a/tests/unit/tokenstorage/v1/test_memory_adapter.py b/tests/unit/tokenstorage/v1/test_memory_adapter.py index d94ff1adc..9402eaaac 100644 --- a/tests/unit/tokenstorage/v1/test_memory_adapter.py +++ b/tests/unit/tokenstorage/v1/test_memory_adapter.py @@ -1,7 +1,7 @@ import time from unittest import mock -from globus_sdk.tokenstorage import MemoryAdapter +from globus_sdk.token_storage import MemoryAdapter def test_memory_adapter_store_overwrites_only_new_data(): diff --git a/tests/unit/tokenstorage/v1/test_simplejson_adapter.py b/tests/unit/tokenstorage/v1/test_simplejson_adapter.py index ecf3c13a7..88d35d28c 100644 --- a/tests/unit/tokenstorage/v1/test_simplejson_adapter.py +++ b/tests/unit/tokenstorage/v1/test_simplejson_adapter.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import __version__ as sdkversion -from globus_sdk.tokenstorage import SimpleJSONFileAdapter +from globus_sdk.token_storage import SimpleJSONFileAdapter def test_simplejson_reading_bad_data(tmp_path): diff --git a/tests/unit/tokenstorage/v1/test_sqlite_adapter.py b/tests/unit/tokenstorage/v1/test_sqlite_adapter.py index f76e7a7c7..d7740af05 100644 --- a/tests/unit/tokenstorage/v1/test_sqlite_adapter.py +++ b/tests/unit/tokenstorage/v1/test_sqlite_adapter.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk.tokenstorage import SQLiteAdapter +from globus_sdk.token_storage import SQLiteAdapter def test_sqlite_reading_bad_config(): diff --git a/tests/unit/tokenstorage/v2/test_token_storage.py b/tests/unit/tokenstorage/v2/test_token_storage.py index 5f9919436..8b25e5548 100644 --- a/tests/unit/tokenstorage/v2/test_token_storage.py +++ b/tests/unit/tokenstorage/v2/test_token_storage.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk.tokenstorage.v2.base import _slugify_app_name +from globus_sdk.token_storage.v2.base import _slugify_app_name @pytest.mark.parametrize( diff --git a/tests/unit/tokenstorage/v2/test_validating_token_storage.py b/tests/unit/tokenstorage/v2/test_validating_token_storage.py index bdba81328..f7ab0046a 100644 --- a/tests/unit/tokenstorage/v2/test_validating_token_storage.py +++ b/tests/unit/tokenstorage/v2/test_validating_token_storage.py @@ -16,13 +16,13 @@ Scope, ) from globus_sdk.scopes.consents import ConsentForest -from globus_sdk.tokenstorage import ( +from globus_sdk.token_storage import ( MemoryTokenStorage, ScopeRequirementsValidator, UnchangingIdentityIDValidator, ValidatingTokenStorage, ) -from globus_sdk.tokenstorage.v2.validating_token_storage import ( +from globus_sdk.token_storage.v2.validating_token_storage import ( IdentityMismatchError, MissingIdentityError, MissingTokenError, From 0896f3b48fd1a8c64e84646419e3dc78812ddd3d Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Thu, 10 Jul 2025 16:56:17 -0500 Subject: [PATCH 085/176] Move `globus_sdk._testing` to `globus_sdk.testing` (#1251) * Renamed _testing to testing * Updated _testing usage sites in src * Updated _testing usage sites in docs * Updated _testing usage sites in tests * Added changelog * Fix: changelog formatting * Fix: docs formatting --- ...829_max.tuecke_sc_26339_rename_testing.rst | 4 ++++ docs/testing/getting_started.rst | 24 +++++++++---------- docs/testing/index.rst | 8 +++---- docs/testing/methods/auth.rst | 4 ++-- docs/testing/methods/flows.rst | 4 ++-- docs/testing/methods/gcs.rst | 4 ++-- docs/testing/methods/groups.rst | 4 ++-- docs/testing/methods/index.rst | 6 ++--- docs/testing/methods/search.rst | 4 ++-- docs/testing/methods/timers.rst | 4 ++-- docs/testing/methods/transfer.rst | 4 ++-- docs/testing/reference.rst | 6 ++--- pyproject.toml | 4 ++-- scripts/ensure_exports_are_documented.py | 2 +- .../directives/enumerate_testing_fixtures.py | 4 ++-- .../directives/expand_testing_fixture.py | 4 ++-- .../{_testing => testing}/__init__.py | 0 .../{_testing => testing}/data/__init__.py | 0 .../data/auth/__init__.py | 0 .../data/auth/_common.py | 0 .../data/auth/create_child_client.py | 2 +- .../data/auth/create_client.py | 2 +- .../data/auth/create_client_credential.py | 2 +- .../data/auth/create_native_app_instance.py | 2 +- .../data/auth/create_policy.py | 2 +- .../data/auth/create_project.py | 2 +- .../data/auth/create_scope.py | 2 +- .../data/auth/delete_client.py | 2 +- .../data/auth/delete_client_credential.py | 2 +- .../data/auth/delete_policy.py | 2 +- .../data/auth/delete_project.py | 2 +- .../data/auth/delete_scope.py | 2 +- .../data/auth/get_client.py | 2 +- .../data/auth/get_client_credentials.py | 2 +- .../data/auth/get_clients.py | 2 +- .../data/auth/get_consents.py | 2 +- .../data/auth/get_identities.py | 2 +- .../data/auth/get_identity_providers.py | 2 +- .../data/auth/get_policies.py | 2 +- .../data/auth/get_policy.py | 2 +- .../data/auth/get_project.py | 2 +- .../data/auth/get_projects.py | 2 +- .../data/auth/get_scope.py | 2 +- .../data/auth/get_scopes.py | 2 +- .../auth/oauth2_client_credentials_tokens.py | 2 +- .../auth/oauth2_exchange_code_for_tokens.py | 2 +- .../data/auth/oauth2_get_dependent_tokens.py | 2 +- .../data/auth/oauth2_revoke_token.py | 2 +- .../data/auth/oauth2_token_introspect.py | 2 +- .../data/auth/oauth2_userinfo.py | 0 .../data/auth/update_client.py | 2 +- .../data/auth/update_policy.py | 2 +- .../data/auth/update_project.py | 2 +- .../data/auth/update_scope.py | 2 +- .../data/auth/userinfo.py | 2 +- .../data/compute/__init__.py | 0 .../data/compute/_common.py | 0 .../data/compute/v2/__init__.py | 0 .../data/compute/v2/delete_endpoint.py | 2 +- .../data/compute/v2/delete_function.py | 2 +- .../data/compute/v2/get_endpoint.py | 2 +- .../data/compute/v2/get_endpoint_status.py | 2 +- .../data/compute/v2/get_endpoints.py | 2 +- .../data/compute/v2/get_function.py | 2 +- .../data/compute/v2/get_result_amqp_url.py | 2 +- .../data/compute/v2/get_task.py | 2 +- .../data/compute/v2/get_task_batch.py | 2 +- .../data/compute/v2/get_task_group.py | 2 +- .../data/compute/v2/get_version.py | 2 +- .../data/compute/v2/lock_endpoint.py | 2 +- .../data/compute/v2/register_endpoint.py | 2 +- .../data/compute/v2/register_function.py | 2 +- .../data/compute/v2/submit.py | 2 +- .../data/compute/v3/__init__.py | 0 .../data/compute/v3/get_endpoint_allowlist.py | 2 +- .../data/compute/v3/lock_endpoint.py | 2 +- .../data/compute/v3/register_endpoint.py | 2 +- .../data/compute/v3/register_function.py | 2 +- .../data/compute/v3/submit.py | 2 +- .../data/compute/v3/update_endpoint.py | 2 +- .../data/flows/__init__.py | 0 .../data/flows/_common.py | 0 .../data/flows/cancel_run.py | 2 +- .../data/flows/create_flow.py | 2 +- .../data/flows/delete_flow.py | 2 +- .../data/flows/delete_run.py | 2 +- .../data/flows/get_flow.py | 2 +- .../data/flows/get_run.py | 2 +- .../data/flows/get_run_definition.py | 2 +- .../data/flows/get_run_logs.py | 2 +- .../data/flows/list_flows.py | 2 +- .../data/flows/list_runs.py | 2 +- .../data/flows/resume_run.py | 2 +- .../data/flows/run_flow.py | 2 +- .../data/flows/update_flow.py | 2 +- .../data/flows/update_run.py | 2 +- .../data/flows/validate_flow.py | 2 +- .../data/flows/validate_run.py | 2 +- .../data/globus_connect_server/__init__.py | 0 .../create_storage_gateway.py | 2 +- .../create_user_credential.py | 2 +- .../delete_storage_gateway.py | 2 +- .../delete_user_credential.py | 2 +- .../get_collection_list.py | 2 +- .../globus_connect_server/get_endpoint.py | 2 +- .../globus_connect_server/get_gcs_info.py | 2 +- .../get_storage_gateway.py | 2 +- .../get_storage_gateway_list.py | 2 +- .../get_user_credential.py | 2 +- .../get_user_credential_list.py | 2 +- .../globus_connect_server/update_endpoint.py | 2 +- .../update_storage_gateway.py | 2 +- .../update_user_credential.py | 2 +- .../data/groups/__init__.py | 0 .../data/groups/_common.py | 0 .../data/groups/create_group.py | 2 +- .../data/groups/delete_group.py | 2 +- .../data/groups/get_group.py | 2 +- .../groups/get_group_by_subscription_id.py | 2 +- .../data/groups/get_my_groups.py | 2 +- .../data/groups/set_group_policies.py | 2 +- .../data/search/__init__.py | 0 .../data/search/batch_delete_by_subject.py | 2 +- .../data/search/create_index.py | 2 +- .../data/search/create_role.py | 2 +- .../data/search/delete_index.py | 2 +- .../data/search/delete_role.py | 2 +- .../data/search/get_role_list.py | 2 +- .../data/search/index_list.py | 2 +- .../data/search/post_search.py | 2 +- .../data/search/reopen_index.py | 2 +- .../data/search/search.py | 2 +- .../data/timer/__init__.py | 0 .../data/timer/_common.py | 0 .../data/timer/create_job.py | 2 +- .../data/timer/create_timer.py | 2 +- .../data/timer/delete_job.py | 2 +- .../data/timer/get_job.py | 2 +- .../data/timer/list_jobs.py | 2 +- .../data/timer/pause_job.py | 2 +- .../data/timer/resume_job.py | 2 +- .../data/timer/update_job.py | 2 +- .../data/transfer/__init__.py | 0 .../data/transfer/_common.py | 0 .../data/transfer/create_endpoint.py | 2 +- .../transfer/endpoint_manager_task_list.py | 2 +- ...point_manager_task_successful_transfers.py | 2 +- .../data/transfer/get_endpoint.py | 2 +- .../data/transfer/get_submission_id.py | 2 +- .../data/transfer/operation_mkdir.py | 2 +- .../data/transfer/operation_rename.py | 2 +- .../data/transfer/operation_stat.py | 2 +- .../set_subscription_admin_verified.py | 2 +- .../data/transfer/set_subscription_id.py | 2 +- .../data/transfer/submit_delete.py | 2 +- .../data/transfer/submit_transfer.py | 2 +- .../data/transfer/task_list.py | 2 +- .../data/transfer/update_endpoint.py | 2 +- .../{_testing => testing}/helpers.py | 0 .../{_testing => testing}/models.py | 0 .../{_testing => testing}/registry.py | 6 ++--- .../base_client/test_advanced_http_options.py | 2 +- .../base_client/test_default_headers.py | 2 +- .../base_client/test_filter_missing.py | 2 +- .../base_client/test_retry_behavior.py | 2 +- .../test_globus_app_token_handling.py | 2 +- .../local_endpoint/test_personal.py | 2 +- .../login_flows/test_login_flow_manager.py | 2 +- .../auth/base/test_oauth2_revoke_token.py | 2 +- .../auth/base/test_oauth2_validate_token.py | 2 +- .../test_create_child_client.py | 2 +- .../test_oauth2_client_credentials_tokens.py | 2 +- .../test_oauth2_get_dependent_tokens.py | 2 +- .../test_oauth2_token_introspect.py | 2 +- .../test_create_native_app_instance.py | 2 +- .../auth/service_client/test_create_client.py | 2 +- .../test_create_client_credential.py | 2 +- .../auth/service_client/test_create_policy.py | 2 +- .../service_client/test_create_project.py | 2 +- .../auth/service_client/test_create_scope.py | 2 +- .../auth/service_client/test_delete_client.py | 2 +- .../test_delete_client_credential.py | 2 +- .../auth/service_client/test_delete_policy.py | 2 +- .../service_client/test_delete_project.py | 2 +- .../auth/service_client/test_delete_scope.py | 2 +- .../auth/service_client/test_get_client.py | 2 +- .../test_get_client_credentials.py | 2 +- .../auth/service_client/test_get_clients.py | 2 +- .../auth/service_client/test_get_consents.py | 2 +- .../service_client/test_get_identities.py | 2 +- .../test_get_identity_providers.py | 2 +- .../auth/service_client/test_get_policies.py | 2 +- .../auth/service_client/test_get_policy.py | 2 +- .../auth/service_client/test_get_project.py | 2 +- .../auth/service_client/test_get_projects.py | 2 +- .../auth/service_client/test_get_scope.py | 2 +- .../auth/service_client/test_get_scopes.py | 2 +- .../auth/service_client/test_update_client.py | 2 +- .../auth/service_client/test_update_policy.py | 2 +- .../service_client/test_update_project.py | 2 +- .../auth/service_client/test_update_scope.py | 2 +- .../auth/service_client/test_userinfo.py | 2 +- .../services/auth/test_auth_client_flow.py | 2 +- .../services/auth/test_identity_map.py | 2 +- .../compute/v2/test_delete_endpoint.py | 2 +- .../compute/v2/test_delete_function.py | 2 +- .../services/compute/v2/test_get_endpoint.py | 2 +- .../compute/v2/test_get_endpoint_status.py | 2 +- .../services/compute/v2/test_get_endpoints.py | 2 +- .../services/compute/v2/test_get_function.py | 2 +- .../compute/v2/test_get_result_amqp_url.py | 2 +- .../compute/v2/test_get_task_batch.py | 2 +- .../compute/v2/test_get_task_group.py | 2 +- .../services/compute/v2/test_get_task_info.py | 2 +- .../services/compute/v2/test_get_version.py | 2 +- .../services/compute/v2/test_lock_endpoint.py | 2 +- .../compute/v2/test_register_endpoint.py | 2 +- .../compute/v2/test_register_function.py | 2 +- .../services/compute/v2/test_submit.py | 2 +- .../compute/v3/test_get_endpoint_allowlist.py | 2 +- .../services/compute/v3/test_lock_endpoint.py | 2 +- .../compute/v3/test_register_endpoint.py | 2 +- .../compute/v3/test_register_function.py | 2 +- .../services/compute/v3/test_submit.py | 2 +- .../compute/v3/test_update_endpoint.py | 2 +- .../services/flows/test_flow_crud.py | 4 ++-- .../services/flows/test_flow_validate.py | 2 +- .../functional/services/flows/test_get_run.py | 2 +- .../services/flows/test_get_run_logs.py | 2 +- .../services/flows/test_list_flows.py | 2 +- .../services/flows/test_list_runs.py | 2 +- .../services/flows/test_resume_run.py | 2 +- .../services/flows/test_run_crud.py | 2 +- .../services/flows/test_run_flow.py | 2 +- .../services/flows/test_validate_run.py | 2 +- .../functional/services/gcs/test_endpoints.py | 2 +- .../services/gcs/test_get_collection_list.py | 2 +- .../services/gcs/test_get_gcs_info.py | 2 +- tests/functional/services/gcs/test_roles.py | 2 +- .../services/gcs/test_storage_gateways.py | 2 +- .../services/gcs/test_user_credential.py | 2 +- .../services/groups/test_create_group.py | 2 +- .../services/groups/test_delete_group.py | 2 +- .../services/groups/test_get_group.py | 2 +- .../test_get_group_by_subscription_id.py | 2 +- .../services/groups/test_get_my_groups.py | 2 +- .../services/groups/test_group_memberships.py | 2 +- .../groups/test_set_group_policies.py | 2 +- .../search/test_batch_delete_by_subject.py | 2 +- .../services/search/test_create_index.py | 2 +- .../services/search/test_delete_index.py | 2 +- .../services/search/test_index_list.py | 2 +- .../services/search/test_reopen_index.py | 2 +- .../functional/services/search/test_search.py | 2 +- .../services/search/test_search_roles.py | 2 +- .../services/timers/test_create_timer.py | 2 +- tests/functional/services/timers/test_jobs.py | 2 +- ...point_manager_task_successful_transfers.py | 2 +- .../endpoint_manager/test_task_event_list.py | 2 +- .../endpoint_manager/test_task_list.py | 2 +- .../services/transfer/test_operation_ls.py | 2 +- .../services/transfer/test_operation_mkdir.py | 2 +- .../transfer/test_operation_rename.py | 2 +- .../services/transfer/test_operation_stat.py | 2 +- .../transfer/test_operation_symlink.py | 2 +- .../test_set_subscription_admin_verified.py | 2 +- .../transfer/test_set_subscription_id.py | 2 +- .../services/transfer/test_simple.py | 2 +- .../services/transfer/test_task_list.py | 2 +- .../services/transfer/test_task_submit.py | 2 +- .../services/transfer/test_task_wait.py | 2 +- .../test_non_default_mock.py | 4 ++-- tests/functional/tokenstorage/v2/conftest.py | 2 +- tests/unit/errors/test_auth_errors.py | 2 +- .../unit/errors/test_common_functionality.py | 2 +- tests/unit/errors/test_timers_errors.py | 2 +- tests/unit/errors/test_transfer_errors.py | 2 +- .../globus_app/test_client_integration.py | 2 +- tests/unit/globus_app/test_globus_app.py | 2 +- .../sphinxext/test_expand_testing_fixture.py | 2 +- tests/unit/test_auth_requirements_error.py | 2 +- tests/unit/test_base_client.py | 2 +- tests/unit/test_gcs_client.py | 2 +- .../test_construct_error.py | 2 +- .../test_registered_response.py | 2 +- 285 files changed, 299 insertions(+), 295 deletions(-) create mode 100644 changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst rename src/globus_sdk/{_testing => testing}/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/auth/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/auth/_common.py (100%) rename src/globus_sdk/{_testing => testing}/data/auth/create_child_client.py (98%) rename src/globus_sdk/{_testing => testing}/data/auth/create_client.py (98%) rename src/globus_sdk/{_testing => testing}/data/auth/create_client_credential.py (93%) rename src/globus_sdk/{_testing => testing}/data/auth/create_native_app_instance.py (97%) rename src/globus_sdk/{_testing => testing}/data/auth/create_policy.py (98%) rename src/globus_sdk/{_testing => testing}/data/auth/create_project.py (96%) rename src/globus_sdk/{_testing => testing}/data/auth/create_scope.py (98%) rename src/globus_sdk/{_testing => testing}/data/auth/delete_client.py (92%) rename src/globus_sdk/{_testing => testing}/data/auth/delete_client_credential.py (88%) rename src/globus_sdk/{_testing => testing}/data/auth/delete_policy.py (90%) rename src/globus_sdk/{_testing => testing}/data/auth/delete_project.py (93%) rename src/globus_sdk/{_testing => testing}/data/auth/delete_scope.py (90%) rename src/globus_sdk/{_testing => testing}/data/auth/get_client.py (93%) rename src/globus_sdk/{_testing => testing}/data/auth/get_client_credentials.py (88%) rename src/globus_sdk/{_testing => testing}/data/auth/get_clients.py (95%) rename src/globus_sdk/{_testing => testing}/data/auth/get_consents.py (96%) rename src/globus_sdk/{_testing => testing}/data/auth/get_identities.py (97%) rename src/globus_sdk/{_testing => testing}/data/auth/get_identity_providers.py (94%) rename src/globus_sdk/{_testing => testing}/data/auth/get_policies.py (94%) rename src/globus_sdk/{_testing => testing}/data/auth/get_policy.py (90%) rename src/globus_sdk/{_testing => testing}/data/auth/get_project.py (95%) rename src/globus_sdk/{_testing => testing}/data/auth/get_projects.py (96%) rename src/globus_sdk/{_testing => testing}/data/auth/get_scope.py (90%) rename src/globus_sdk/{_testing => testing}/data/auth/get_scopes.py (95%) rename src/globus_sdk/{_testing => testing}/data/auth/oauth2_client_credentials_tokens.py (94%) rename src/globus_sdk/{_testing => testing}/data/auth/oauth2_exchange_code_for_tokens.py (94%) rename src/globus_sdk/{_testing => testing}/data/auth/oauth2_get_dependent_tokens.py (96%) rename src/globus_sdk/{_testing => testing}/data/auth/oauth2_revoke_token.py (72%) rename src/globus_sdk/{_testing => testing}/data/auth/oauth2_token_introspect.py (94%) rename src/globus_sdk/{_testing => testing}/data/auth/oauth2_userinfo.py (100%) rename src/globus_sdk/{_testing => testing}/data/auth/update_client.py (97%) rename src/globus_sdk/{_testing => testing}/data/auth/update_policy.py (98%) rename src/globus_sdk/{_testing => testing}/data/auth/update_project.py (96%) rename src/globus_sdk/{_testing => testing}/data/auth/update_scope.py (98%) rename src/globus_sdk/{_testing => testing}/data/auth/userinfo.py (91%) rename src/globus_sdk/{_testing => testing}/data/compute/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/compute/_common.py (100%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/delete_endpoint.py (79%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/delete_function.py (79%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_endpoint.py (92%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_endpoint_status.py (87%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_endpoints.py (95%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_function.py (89%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_result_amqp_url.py (81%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_task.py (78%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_task_batch.py (87%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_task_group.py (89%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/get_version.py (90%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/lock_endpoint.py (85%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/register_endpoint.py (90%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/register_function.py (84%) rename src/globus_sdk/{_testing => testing}/data/compute/v2/submit.py (91%) rename src/globus_sdk/{_testing => testing}/data/compute/v3/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/compute/v3/get_endpoint_allowlist.py (86%) rename src/globus_sdk/{_testing => testing}/data/compute/v3/lock_endpoint.py (85%) rename src/globus_sdk/{_testing => testing}/data/compute/v3/register_endpoint.py (89%) rename src/globus_sdk/{_testing => testing}/data/compute/v3/register_function.py (86%) rename src/globus_sdk/{_testing => testing}/data/compute/v3/submit.py (91%) rename src/globus_sdk/{_testing => testing}/data/compute/v3/update_endpoint.py (90%) rename src/globus_sdk/{_testing => testing}/data/flows/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/flows/_common.py (100%) rename src/globus_sdk/{_testing => testing}/data/flows/cancel_run.py (91%) rename src/globus_sdk/{_testing => testing}/data/flows/create_flow.py (96%) rename src/globus_sdk/{_testing => testing}/data/flows/delete_flow.py (95%) rename src/globus_sdk/{_testing => testing}/data/flows/delete_run.py (94%) rename src/globus_sdk/{_testing => testing}/data/flows/get_flow.py (84%) rename src/globus_sdk/{_testing => testing}/data/flows/get_run.py (92%) rename src/globus_sdk/{_testing => testing}/data/flows/get_run_definition.py (86%) rename src/globus_sdk/{_testing => testing}/data/flows/get_run_logs.py (99%) rename src/globus_sdk/{_testing => testing}/data/flows/list_flows.py (98%) rename src/globus_sdk/{_testing => testing}/data/flows/list_runs.py (98%) rename src/globus_sdk/{_testing => testing}/data/flows/resume_run.py (85%) rename src/globus_sdk/{_testing => testing}/data/flows/run_flow.py (93%) rename src/globus_sdk/{_testing => testing}/data/flows/update_flow.py (92%) rename src/globus_sdk/{_testing => testing}/data/flows/update_run.py (84%) rename src/globus_sdk/{_testing => testing}/data/flows/validate_flow.py (97%) rename src/globus_sdk/{_testing => testing}/data/flows/validate_run.py (97%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/create_storage_gateway.py (96%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/create_user_credential.py (94%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/delete_storage_gateway.py (93%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/delete_user_credential.py (88%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/get_collection_list.py (97%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/get_endpoint.py (94%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/get_gcs_info.py (93%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/get_storage_gateway.py (95%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/get_storage_gateway_list.py (97%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/get_user_credential.py (94%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/get_user_credential_list.py (96%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/update_endpoint.py (95%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/update_storage_gateway.py (95%) rename src/globus_sdk/{_testing => testing}/data/globus_connect_server/update_user_credential.py (94%) rename src/globus_sdk/{_testing => testing}/data/groups/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/groups/_common.py (100%) rename src/globus_sdk/{_testing => testing}/data/groups/create_group.py (78%) rename src/globus_sdk/{_testing => testing}/data/groups/delete_group.py (79%) rename src/globus_sdk/{_testing => testing}/data/groups/get_group.py (89%) rename src/globus_sdk/{_testing => testing}/data/groups/get_group_by_subscription_id.py (90%) rename src/globus_sdk/{_testing => testing}/data/groups/get_my_groups.py (98%) rename src/globus_sdk/{_testing => testing}/data/groups/set_group_policies.py (88%) rename src/globus_sdk/{_testing => testing}/data/search/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/search/batch_delete_by_subject.py (82%) rename src/globus_sdk/{_testing => testing}/data/search/create_index.py (95%) rename src/globus_sdk/{_testing => testing}/data/search/create_role.py (92%) rename src/globus_sdk/{_testing => testing}/data/search/delete_index.py (92%) rename src/globus_sdk/{_testing => testing}/data/search/delete_role.py (91%) rename src/globus_sdk/{_testing => testing}/data/search/get_role_list.py (94%) rename src/globus_sdk/{_testing => testing}/data/search/index_list.py (95%) rename src/globus_sdk/{_testing => testing}/data/search/post_search.py (92%) rename src/globus_sdk/{_testing => testing}/data/search/reopen_index.py (93%) rename src/globus_sdk/{_testing => testing}/data/search/search.py (92%) rename src/globus_sdk/{_testing => testing}/data/timer/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/timer/_common.py (100%) rename src/globus_sdk/{_testing => testing}/data/timer/create_job.py (93%) rename src/globus_sdk/{_testing => testing}/data/timer/create_timer.py (86%) rename src/globus_sdk/{_testing => testing}/data/timer/delete_job.py (77%) rename src/globus_sdk/{_testing => testing}/data/timer/get_job.py (96%) rename src/globus_sdk/{_testing => testing}/data/timer/list_jobs.py (77%) rename src/globus_sdk/{_testing => testing}/data/timer/pause_job.py (80%) rename src/globus_sdk/{_testing => testing}/data/timer/resume_job.py (80%) rename src/globus_sdk/{_testing => testing}/data/timer/update_job.py (85%) rename src/globus_sdk/{_testing => testing}/data/transfer/__init__.py (100%) rename src/globus_sdk/{_testing => testing}/data/transfer/_common.py (100%) rename src/globus_sdk/{_testing => testing}/data/transfer/create_endpoint.py (89%) rename src/globus_sdk/{_testing => testing}/data/transfer/endpoint_manager_task_list.py (98%) rename src/globus_sdk/{_testing => testing}/data/transfer/endpoint_manager_task_successful_transfers.py (90%) rename src/globus_sdk/{_testing => testing}/data/transfer/get_endpoint.py (95%) rename src/globus_sdk/{_testing => testing}/data/transfer/get_submission_id.py (78%) rename src/globus_sdk/{_testing => testing}/data/transfer/operation_mkdir.py (88%) rename src/globus_sdk/{_testing => testing}/data/transfer/operation_rename.py (88%) rename src/globus_sdk/{_testing => testing}/data/transfer/operation_stat.py (96%) rename src/globus_sdk/{_testing => testing}/data/transfer/set_subscription_admin_verified.py (97%) rename src/globus_sdk/{_testing => testing}/data/transfer/set_subscription_id.py (96%) rename src/globus_sdk/{_testing => testing}/data/transfer/submit_delete.py (92%) rename src/globus_sdk/{_testing => testing}/data/transfer/submit_transfer.py (95%) rename src/globus_sdk/{_testing => testing}/data/transfer/task_list.py (97%) rename src/globus_sdk/{_testing => testing}/data/transfer/update_endpoint.py (87%) rename src/globus_sdk/{_testing => testing}/helpers.py (100%) rename src/globus_sdk/{_testing => testing}/models.py (100%) rename src/globus_sdk/{_testing => testing}/registry.py (96%) rename tests/functional/{_testing => testing}/test_non_default_mock.py (91%) rename tests/unit/{_testing => testing}/test_construct_error.py (97%) rename tests/unit/{_testing => testing}/test_registered_response.py (91%) diff --git a/changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst b/changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst new file mode 100644 index 000000000..63d4be747 --- /dev/null +++ b/changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst @@ -0,0 +1,4 @@ +Changed +------- + +- Renamed the ``globus_sdk._testing`` subpackage to ``globus_sdk.testing``. (:pr:`1251`) diff --git a/docs/testing/getting_started.rst b/docs/testing/getting_started.rst index 8ace2283f..d274f4d5c 100644 --- a/docs/testing/getting_started.rst +++ b/docs/testing/getting_started.rst @@ -3,15 +3,15 @@ This component is an *alpha*. Interfaces may change outside of the normal semver policy. -Getting Started with _testing -============================= +Getting Started with testing +============================ Dependencies ------------ This toolchain requires the ``responses`` library. -``globus_sdk._testing`` is tested to operate with the latest version of +``globus_sdk.testing`` is tested to operate with the latest version of ``responses``. Recommended Fixtures @@ -41,7 +41,7 @@ activated by name: .. code-block:: python - from globus_sdk._testing import load_response + from globus_sdk.testing import load_response # load_response will add the response to `responses` and return it load_response("auth.get_identities") @@ -54,7 +54,7 @@ unbound, as in: .. code-block:: python import globus_sdk - from globus_sdk._testing import load_response + from globus_sdk.testing import load_response load_response(globus_sdk.AuthClient.get_identities) load_response(globus_sdk.AuthClient.get_identities, case="unauthorized") @@ -71,7 +71,7 @@ load all of them at once: .. code-block:: python - from globus_sdk._testing import load_response_set + from globus_sdk.testing import load_response_set fixtures = load_response_set("scenario.foo") @@ -86,7 +86,7 @@ response is ``"default"``. .. code-block:: python from globus_sdk import AuthClient - from globus_sdk._testing import get_response_set + from globus_sdk.testing import get_response_set # rset will not be activated rset = get_response_set(AuthClient.get_identities) @@ -110,7 +110,7 @@ override the builtin response sets, if names match. .. code-block:: python - from globus_sdk._testing import load_response_set, register_response_set + from globus_sdk.testing import load_response_set, register_response_set import uuid # register a scenario under which Globus Auth get_identities and Globus @@ -152,7 +152,7 @@ Loading Responses without Registering Because ``RegisteredResponse`` takes care of resolving ``"auth"`` to the Auth URL, ``"transfer"`` to the Transfer URL, and so forth, you might want to use -``globus_sdk._testing`` in lieu of ``responses`` even when registering single +``globus_sdk.testing`` in lieu of ``responses`` even when registering single responses for individual tests. To support this mode of usage, ``load_response`` can take a @@ -165,7 +165,7 @@ Consider the following example of a parametrized test which uses .. code-block:: python - from globus_sdk._testing import load_response, RegisteredResponse + from globus_sdk.testing import load_response, RegisteredResponse import pytest @@ -191,7 +191,7 @@ outside of the specific test. Using non-default responses.RequestsMock objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -By default, all methods in ``globus_sdk._testing`` which converse with +By default, all methods in ``globus_sdk.testing`` which converse with ``responses`` use the default mock. This is the behavior offered by ``responses.add(...)`` and similar methods. @@ -208,7 +208,7 @@ e.g. .. code-block:: python - from globus_sdk._testing import get_last_request + from globus_sdk.testing import get_last_request import responses custom_mock = responses.RequestsMock(...) diff --git a/docs/testing/index.rst b/docs/testing/index.rst index 9de0abb21..47d97fb40 100644 --- a/docs/testing/index.rst +++ b/docs/testing/index.rst @@ -3,14 +3,14 @@ This component is an *alpha*. Interfaces may change outside of the normal semver policy. -.. _testing_root: +.. testing_root: -Globus SDK _testing -=================== +Globus SDK testing +================== .. warning:: - The exact data and payloads provided via ``_testing`` are a best + The exact data and payloads provided via ``testing`` are a best approximation of API responses. They may change in any SDK release to be more accurate. diff --git a/docs/testing/methods/auth.rst b/docs/testing/methods/auth.rst index 538af2410..0c43fb93b 100644 --- a/docs/testing/methods/auth.rst +++ b/docs/testing/methods/auth.rst @@ -1,4 +1,4 @@ -Globus Auth _testing Method List -================================ +Globus Auth testing Method List +=============================== .. enumeratetestingfixtures:: globus_sdk.AuthClient diff --git a/docs/testing/methods/flows.rst b/docs/testing/methods/flows.rst index 65d6587b9..38401ba9e 100644 --- a/docs/testing/methods/flows.rst +++ b/docs/testing/methods/flows.rst @@ -1,5 +1,5 @@ -Globus Flows _testing Method List -================================= +Globus Flows testing Method List +================================ .. enumeratetestingfixtures:: globus_sdk.FlowsClient diff --git a/docs/testing/methods/gcs.rst b/docs/testing/methods/gcs.rst index f9f32bc92..dbdb157ed 100644 --- a/docs/testing/methods/gcs.rst +++ b/docs/testing/methods/gcs.rst @@ -1,4 +1,4 @@ -Globus Connect Server _testing Method List -========================================== +Globus Connect Server testing Method List +========================================= .. enumeratetestingfixtures:: globus_sdk.GCSClient diff --git a/docs/testing/methods/groups.rst b/docs/testing/methods/groups.rst index cf0885ef8..f60978d55 100644 --- a/docs/testing/methods/groups.rst +++ b/docs/testing/methods/groups.rst @@ -1,4 +1,4 @@ -Globus Groups _testing Method List -================================== +Globus Groups testing Method List +================================= .. enumeratetestingfixtures:: globus_sdk.GroupsClient diff --git a/docs/testing/methods/index.rst b/docs/testing/methods/index.rst index a47b86348..33dff9633 100644 --- a/docs/testing/methods/index.rst +++ b/docs/testing/methods/index.rst @@ -3,10 +3,10 @@ This component is an *alpha*. Interfaces may change outside of the normal semver policy. -_testing Method List -==================== +testing Method List +=================== -These pages list all methods which have ``globus_sdk._testing`` response data, +These pages list all methods which have ``globus_sdk.testing`` response data, and the casenames for those data. .. toctree:: diff --git a/docs/testing/methods/search.rst b/docs/testing/methods/search.rst index effa27c3a..be09ad003 100644 --- a/docs/testing/methods/search.rst +++ b/docs/testing/methods/search.rst @@ -1,4 +1,4 @@ -Globus Search _testing Method List -================================== +Globus Search testing Method List +================================= .. enumeratetestingfixtures:: globus_sdk.SearchClient diff --git a/docs/testing/methods/timers.rst b/docs/testing/methods/timers.rst index 638982725..301376fed 100644 --- a/docs/testing/methods/timers.rst +++ b/docs/testing/methods/timers.rst @@ -1,4 +1,4 @@ -Globus Timers _testing Method List -================================== +Globus Timers testing Method List +================================= .. enumeratetestingfixtures:: globus_sdk.TimersClient diff --git a/docs/testing/methods/transfer.rst b/docs/testing/methods/transfer.rst index d6b71a2e5..8b52f6976 100644 --- a/docs/testing/methods/transfer.rst +++ b/docs/testing/methods/transfer.rst @@ -1,4 +1,4 @@ -Globus Transfer _testing Method List -==================================== +Globus Transfer testing Method List +=================================== .. enumeratetestingfixtures:: globus_sdk.TransferClient diff --git a/docs/testing/reference.rst b/docs/testing/reference.rst index 51a5f9d1a..490935add 100644 --- a/docs/testing/reference.rst +++ b/docs/testing/reference.rst @@ -3,10 +3,10 @@ This component is an *alpha*. Interfaces may change outside of the normal semver policy. -_testing Reference -================== +testing Reference +================= -.. module:: globus_sdk._testing +.. module:: globus_sdk.testing Functions --------- diff --git a/pyproject.toml b/pyproject.toml index 93ae978d4..e093dc324 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ typing = [ "types-requests", "typing-extensions>=4.0", # although 'responses' is provided by the test requirements, it also - # must be installed for proper type-checking on globus_sdk._testing + # must be installed for proper type-checking on globus_sdk.testing "responses", # similarly, sphinx is needed to type-check our sphinx extension "sphinx", @@ -120,7 +120,7 @@ source = ["globus_sdk"] # omit must be specified in a way which matches the # tox environment installations, so lead with `**` omit = [ - "**/globus_sdk/_testing/*", + "**/globus_sdk/testing/*", ] [tool.coverage.paths] diff --git a/scripts/ensure_exports_are_documented.py b/scripts/ensure_exports_are_documented.py index f871211c3..917e2ad4f 100755 --- a/scripts/ensure_exports_are_documented.py +++ b/scripts/ensure_exports_are_documented.py @@ -20,7 +20,7 @@ "globus_sdk/globus_app/", "globus_sdk/scopes/", "globus_sdk/response.py", - "globus_sdk/_testing/", + "globus_sdk/testing/", ) DEPRECATED_NAMES = { diff --git a/src/globus_sdk/_sphinxext/directives/enumerate_testing_fixtures.py b/src/globus_sdk/_sphinxext/directives/enumerate_testing_fixtures.py index f6194a555..9734bee9e 100644 --- a/src/globus_sdk/_sphinxext/directives/enumerate_testing_fixtures.py +++ b/src/globus_sdk/_sphinxext/directives/enumerate_testing_fixtures.py @@ -17,7 +17,7 @@ class EnumerateTestingFixtures(AddContentDirective): } def gen_rst(self) -> t.Iterator[str]: - from globus_sdk._testing import get_response_set + from globus_sdk.testing import get_response_set underline_char = self.options.get("header_underline_char", "-") @@ -46,7 +46,7 @@ def gen_rst(self) -> t.Iterator[str]: for casename in rset.cases(): # use "attr" rather than "meth" so that sphinx does not add parens # the use of the method as an attribute of the class or instance better - # matches how `_testing` handles things + # matches how `testing` handles things yield ( ".. dropdown:: " f':py:attr:`~{classname}.{methodname}` (``case="{casename}"``)' diff --git a/src/globus_sdk/_sphinxext/directives/expand_testing_fixture.py b/src/globus_sdk/_sphinxext/directives/expand_testing_fixture.py index f41450db5..0bcf109cc 100644 --- a/src/globus_sdk/_sphinxext/directives/expand_testing_fixture.py +++ b/src/globus_sdk/_sphinxext/directives/expand_testing_fixture.py @@ -3,7 +3,7 @@ from docutils.parsers.rst import directives -from globus_sdk._testing import ResponseList +from globus_sdk.testing import ResponseList from .add_content_directive import AddContentDirective @@ -17,7 +17,7 @@ class ExpandTestingFixture(AddContentDirective): } def gen_rst(self) -> t.Iterator[str]: - from globus_sdk._testing import get_response_set + from globus_sdk.testing import get_response_set response_set_name = self.arguments[0] casename = "default" diff --git a/src/globus_sdk/_testing/__init__.py b/src/globus_sdk/testing/__init__.py similarity index 100% rename from src/globus_sdk/_testing/__init__.py rename to src/globus_sdk/testing/__init__.py diff --git a/src/globus_sdk/_testing/data/__init__.py b/src/globus_sdk/testing/data/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/__init__.py rename to src/globus_sdk/testing/data/__init__.py diff --git a/src/globus_sdk/_testing/data/auth/__init__.py b/src/globus_sdk/testing/data/auth/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/auth/__init__.py rename to src/globus_sdk/testing/data/auth/__init__.py diff --git a/src/globus_sdk/_testing/data/auth/_common.py b/src/globus_sdk/testing/data/auth/_common.py similarity index 100% rename from src/globus_sdk/_testing/data/auth/_common.py rename to src/globus_sdk/testing/data/auth/_common.py diff --git a/src/globus_sdk/_testing/data/auth/create_child_client.py b/src/globus_sdk/testing/data/auth/create_child_client.py similarity index 98% rename from src/globus_sdk/_testing/data/auth/create_child_client.py rename to src/globus_sdk/testing/data/auth/create_child_client.py index 8a38df251..e3f89d124 100644 --- a/src/globus_sdk/_testing/data/auth/create_child_client.py +++ b/src/globus_sdk/testing/data/auth/create_child_client.py @@ -3,7 +3,7 @@ from responses.matchers import json_params_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet _COMMON_RESPONSE_RECORD = { "fqdns": [], diff --git a/src/globus_sdk/_testing/data/auth/create_client.py b/src/globus_sdk/testing/data/auth/create_client.py similarity index 98% rename from src/globus_sdk/_testing/data/auth/create_client.py rename to src/globus_sdk/testing/data/auth/create_client.py index 64274c70d..fcdd4e9ce 100644 --- a/src/globus_sdk/_testing/data/auth/create_client.py +++ b/src/globus_sdk/testing/data/auth/create_client.py @@ -3,7 +3,7 @@ from responses.matchers import json_params_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet _COMMON_RESPONSE_RECORD = { "fqdns": [], diff --git a/src/globus_sdk/_testing/data/auth/create_client_credential.py b/src/globus_sdk/testing/data/auth/create_client_credential.py similarity index 93% rename from src/globus_sdk/_testing/data/auth/create_client_credential.py rename to src/globus_sdk/testing/data/auth/create_client_credential.py index 19f43fd5b..29ff4344d 100644 --- a/src/globus_sdk/_testing/data/auth/create_client_credential.py +++ b/src/globus_sdk/testing/data/auth/create_client_credential.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet NEW_CREDENTIAL_NAME = str(uuid.uuid4()).replace("-", "") diff --git a/src/globus_sdk/_testing/data/auth/create_native_app_instance.py b/src/globus_sdk/testing/data/auth/create_native_app_instance.py similarity index 97% rename from src/globus_sdk/_testing/data/auth/create_native_app_instance.py rename to src/globus_sdk/testing/data/auth/create_native_app_instance.py index cc9637ac2..d50a31d5c 100644 --- a/src/globus_sdk/_testing/data/auth/create_native_app_instance.py +++ b/src/globus_sdk/testing/data/auth/create_native_app_instance.py @@ -3,7 +3,7 @@ from responses.matchers import json_params_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet APP_REQUEST_ARGS = { "template_id": str(uuid.uuid1()), diff --git a/src/globus_sdk/_testing/data/auth/create_policy.py b/src/globus_sdk/testing/data/auth/create_policy.py similarity index 98% rename from src/globus_sdk/_testing/data/auth/create_policy.py rename to src/globus_sdk/testing/data/auth/create_policy.py index c1934ecf1..a97fe5d1c 100644 --- a/src/globus_sdk/_testing/data/auth/create_policy.py +++ b/src/globus_sdk/testing/data/auth/create_policy.py @@ -3,7 +3,7 @@ from responses.matchers import json_params_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet POLICY_REQUEST_ARGS = { "project_id": str(uuid.uuid1()), diff --git a/src/globus_sdk/_testing/data/auth/create_project.py b/src/globus_sdk/testing/data/auth/create_project.py similarity index 96% rename from src/globus_sdk/_testing/data/auth/create_project.py rename to src/globus_sdk/testing/data/auth/create_project.py index 5e2a2b0f0..3d2ac5de7 100644 --- a/src/globus_sdk/_testing/data/auth/create_project.py +++ b/src/globus_sdk/testing/data/auth/create_project.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet project_id = str(uuid.uuid1()) star_lord = { diff --git a/src/globus_sdk/_testing/data/auth/create_scope.py b/src/globus_sdk/testing/data/auth/create_scope.py similarity index 98% rename from src/globus_sdk/_testing/data/auth/create_scope.py rename to src/globus_sdk/testing/data/auth/create_scope.py index 9f217709b..95ba4958d 100644 --- a/src/globus_sdk/_testing/data/auth/create_scope.py +++ b/src/globus_sdk/testing/data/auth/create_scope.py @@ -4,7 +4,7 @@ from responses.matchers import json_params_matcher from globus_sdk import DependentScopeSpec -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet SCOPE_REQUEST_ARGS = { "client_id": str(uuid.uuid1()), diff --git a/src/globus_sdk/_testing/data/auth/delete_client.py b/src/globus_sdk/testing/data/auth/delete_client.py similarity index 92% rename from src/globus_sdk/_testing/data/auth/delete_client.py rename to src/globus_sdk/testing/data/auth/delete_client.py index f5916df0a..499222aff 100644 --- a/src/globus_sdk/_testing/data/auth/delete_client.py +++ b/src/globus_sdk/testing/data/auth/delete_client.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CLIENT = { "required_idp": None, diff --git a/src/globus_sdk/_testing/data/auth/delete_client_credential.py b/src/globus_sdk/testing/data/auth/delete_client_credential.py similarity index 88% rename from src/globus_sdk/_testing/data/auth/delete_client_credential.py rename to src/globus_sdk/testing/data/auth/delete_client_credential.py index ac407af21..68a1bae7d 100644 --- a/src/globus_sdk/_testing/data/auth/delete_client_credential.py +++ b/src/globus_sdk/testing/data/auth/delete_client_credential.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CREDENTIAL = { "name": "foo", diff --git a/src/globus_sdk/_testing/data/auth/delete_policy.py b/src/globus_sdk/testing/data/auth/delete_policy.py similarity index 90% rename from src/globus_sdk/_testing/data/auth/delete_policy.py rename to src/globus_sdk/testing/data/auth/delete_policy.py index 6aa91ab50..f0924fbab 100644 --- a/src/globus_sdk/_testing/data/auth/delete_policy.py +++ b/src/globus_sdk/testing/data/auth/delete_policy.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet POLICY = { "high_assurance": False, diff --git a/src/globus_sdk/_testing/data/auth/delete_project.py b/src/globus_sdk/testing/data/auth/delete_project.py similarity index 93% rename from src/globus_sdk/_testing/data/auth/delete_project.py rename to src/globus_sdk/testing/data/auth/delete_project.py index fb152e510..12d570f11 100644 --- a/src/globus_sdk/_testing/data/auth/delete_project.py +++ b/src/globus_sdk/testing/data/auth/delete_project.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet project_id = str(uuid.uuid1()) star_lord = { diff --git a/src/globus_sdk/_testing/data/auth/delete_scope.py b/src/globus_sdk/testing/data/auth/delete_scope.py similarity index 90% rename from src/globus_sdk/_testing/data/auth/delete_scope.py rename to src/globus_sdk/testing/data/auth/delete_scope.py index 67aaee1da..f7afd4e57 100644 --- a/src/globus_sdk/_testing/data/auth/delete_scope.py +++ b/src/globus_sdk/testing/data/auth/delete_scope.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet SCOPE = { "scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manager", # noqa: E501 diff --git a/src/globus_sdk/_testing/data/auth/get_client.py b/src/globus_sdk/testing/data/auth/get_client.py similarity index 93% rename from src/globus_sdk/_testing/data/auth/get_client.py rename to src/globus_sdk/testing/data/auth/get_client.py index d422b7f06..046823d26 100644 --- a/src/globus_sdk/_testing/data/auth/get_client.py +++ b/src/globus_sdk/testing/data/auth/get_client.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet FQDN = "globus.org" diff --git a/src/globus_sdk/_testing/data/auth/get_client_credentials.py b/src/globus_sdk/testing/data/auth/get_client_credentials.py similarity index 88% rename from src/globus_sdk/_testing/data/auth/get_client_credentials.py rename to src/globus_sdk/testing/data/auth/get_client_credentials.py index a86bffb49..706018fb7 100644 --- a/src/globus_sdk/_testing/data/auth/get_client_credentials.py +++ b/src/globus_sdk/testing/data/auth/get_client_credentials.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CREDENTIAL = { "name": "foo", diff --git a/src/globus_sdk/_testing/data/auth/get_clients.py b/src/globus_sdk/testing/data/auth/get_clients.py similarity index 95% rename from src/globus_sdk/_testing/data/auth/get_clients.py rename to src/globus_sdk/testing/data/auth/get_clients.py index 8599015ab..746217e49 100644 --- a/src/globus_sdk/_testing/data/auth/get_clients.py +++ b/src/globus_sdk/testing/data/auth/get_clients.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet FOO_CLIENT = { "required_idp": None, diff --git a/src/globus_sdk/_testing/data/auth/get_consents.py b/src/globus_sdk/testing/data/auth/get_consents.py similarity index 96% rename from src/globus_sdk/_testing/data/auth/get_consents.py rename to src/globus_sdk/testing/data/auth/get_consents.py index 3270cb528..1068f8f5d 100644 --- a/src/globus_sdk/_testing/data/auth/get_consents.py +++ b/src/globus_sdk/testing/data/auth/get_consents.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet _DATA_ACCESS = ( "https://auth.globus.org/scopes/542a86fc-1766-450d-841f-065488a2ec01/data_access" diff --git a/src/globus_sdk/_testing/data/auth/get_identities.py b/src/globus_sdk/testing/data/auth/get_identities.py similarity index 97% rename from src/globus_sdk/_testing/data/auth/get_identities.py rename to src/globus_sdk/testing/data/auth/get_identities.py index f7b530882..1c281f790 100644 --- a/src/globus_sdk/_testing/data/auth/get_identities.py +++ b/src/globus_sdk/testing/data/auth/get_identities.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import UNAUTHORIZED_AUTH_RESPONSE diff --git a/src/globus_sdk/_testing/data/auth/get_identity_providers.py b/src/globus_sdk/testing/data/auth/get_identity_providers.py similarity index 94% rename from src/globus_sdk/_testing/data/auth/get_identity_providers.py rename to src/globus_sdk/testing/data/auth/get_identity_providers.py index 7e632f7c0..82043b12b 100644 --- a/src/globus_sdk/_testing/data/auth/get_identity_providers.py +++ b/src/globus_sdk/testing/data/auth/get_identity_providers.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet globusid_idp = { "short_name": "globusid", diff --git a/src/globus_sdk/_testing/data/auth/get_policies.py b/src/globus_sdk/testing/data/auth/get_policies.py similarity index 94% rename from src/globus_sdk/_testing/data/auth/get_policies.py rename to src/globus_sdk/testing/data/auth/get_policies.py index 086d2ce55..25d76bb27 100644 --- a/src/globus_sdk/_testing/data/auth/get_policies.py +++ b/src/globus_sdk/testing/data/auth/get_policies.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet GREEN_LIGHT_POLICY = { "high_assurance": False, diff --git a/src/globus_sdk/_testing/data/auth/get_policy.py b/src/globus_sdk/testing/data/auth/get_policy.py similarity index 90% rename from src/globus_sdk/_testing/data/auth/get_policy.py rename to src/globus_sdk/testing/data/auth/get_policy.py index 916da351a..ab36fa5d2 100644 --- a/src/globus_sdk/_testing/data/auth/get_policy.py +++ b/src/globus_sdk/testing/data/auth/get_policy.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet POLICY = { "high_assurance": False, diff --git a/src/globus_sdk/_testing/data/auth/get_project.py b/src/globus_sdk/testing/data/auth/get_project.py similarity index 95% rename from src/globus_sdk/_testing/data/auth/get_project.py rename to src/globus_sdk/testing/data/auth/get_project.py index 0306d9baa..2471df49a 100644 --- a/src/globus_sdk/_testing/data/auth/get_project.py +++ b/src/globus_sdk/testing/data/auth/get_project.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet GUARDIANS_IDP_ID = str(uuid.uuid1()) STAR_LORD = { diff --git a/src/globus_sdk/_testing/data/auth/get_projects.py b/src/globus_sdk/testing/data/auth/get_projects.py similarity index 96% rename from src/globus_sdk/_testing/data/auth/get_projects.py rename to src/globus_sdk/testing/data/auth/get_projects.py index a81f65d5a..1d2f8907b 100644 --- a/src/globus_sdk/_testing/data/auth/get_projects.py +++ b/src/globus_sdk/testing/data/auth/get_projects.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet GUARDIANS_IDP_ID = str(uuid.uuid1()) STAR_LORD = { diff --git a/src/globus_sdk/_testing/data/auth/get_scope.py b/src/globus_sdk/testing/data/auth/get_scope.py similarity index 90% rename from src/globus_sdk/_testing/data/auth/get_scope.py rename to src/globus_sdk/testing/data/auth/get_scope.py index e7bd40b2b..a40d13945 100644 --- a/src/globus_sdk/_testing/data/auth/get_scope.py +++ b/src/globus_sdk/testing/data/auth/get_scope.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet SCOPE = { "scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manager", # noqa: E501 diff --git a/src/globus_sdk/_testing/data/auth/get_scopes.py b/src/globus_sdk/testing/data/auth/get_scopes.py similarity index 95% rename from src/globus_sdk/_testing/data/auth/get_scopes.py rename to src/globus_sdk/testing/data/auth/get_scopes.py index 0b4e195a8..d8c0431e0 100644 --- a/src/globus_sdk/_testing/data/auth/get_scopes.py +++ b/src/globus_sdk/testing/data/auth/get_scopes.py @@ -2,7 +2,7 @@ from responses.matchers import query_param_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet SCOPE1 = { "scope_string": "https://auth.globus.org/scopes/3f33d83f-ec0a-4190-887d-0622e7c4ee9a/manage", # noqa: E501 diff --git a/src/globus_sdk/_testing/data/auth/oauth2_client_credentials_tokens.py b/src/globus_sdk/testing/data/auth/oauth2_client_credentials_tokens.py similarity index 94% rename from src/globus_sdk/_testing/data/auth/oauth2_client_credentials_tokens.py rename to src/globus_sdk/testing/data/auth/oauth2_client_credentials_tokens.py index be0f416ab..e0d046d38 100644 --- a/src/globus_sdk/_testing/data/auth/oauth2_client_credentials_tokens.py +++ b/src/globus_sdk/testing/data/auth/oauth2_client_credentials_tokens.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet _token = "DUMMY_TRANSFER_TOKEN_FROM_THE_INTERTUBES" _scope = "urn:globus:auth:scope:transfer.api.globus.org:all" diff --git a/src/globus_sdk/_testing/data/auth/oauth2_exchange_code_for_tokens.py b/src/globus_sdk/testing/data/auth/oauth2_exchange_code_for_tokens.py similarity index 94% rename from src/globus_sdk/_testing/data/auth/oauth2_exchange_code_for_tokens.py rename to src/globus_sdk/testing/data/auth/oauth2_exchange_code_for_tokens.py index e691e087f..e7b8bbce6 100644 --- a/src/globus_sdk/_testing/data/auth/oauth2_exchange_code_for_tokens.py +++ b/src/globus_sdk/testing/data/auth/oauth2_exchange_code_for_tokens.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet RESPONSES = ResponseSet( default=RegisteredResponse( diff --git a/src/globus_sdk/_testing/data/auth/oauth2_get_dependent_tokens.py b/src/globus_sdk/testing/data/auth/oauth2_get_dependent_tokens.py similarity index 96% rename from src/globus_sdk/_testing/data/auth/oauth2_get_dependent_tokens.py rename to src/globus_sdk/testing/data/auth/oauth2_get_dependent_tokens.py index b9c06d250..994d36741 100644 --- a/src/globus_sdk/_testing/data/auth/oauth2_get_dependent_tokens.py +++ b/src/globus_sdk/testing/data/auth/oauth2_get_dependent_tokens.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet RESPONSES = ResponseSet( groups=RegisteredResponse( diff --git a/src/globus_sdk/_testing/data/auth/oauth2_revoke_token.py b/src/globus_sdk/testing/data/auth/oauth2_revoke_token.py similarity index 72% rename from src/globus_sdk/_testing/data/auth/oauth2_revoke_token.py rename to src/globus_sdk/testing/data/auth/oauth2_revoke_token.py index 78005c482..f66e4439b 100644 --- a/src/globus_sdk/_testing/data/auth/oauth2_revoke_token.py +++ b/src/globus_sdk/testing/data/auth/oauth2_revoke_token.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet RESPONSES = ResponseSet( default=RegisteredResponse( diff --git a/src/globus_sdk/_testing/data/auth/oauth2_token_introspect.py b/src/globus_sdk/testing/data/auth/oauth2_token_introspect.py similarity index 94% rename from src/globus_sdk/_testing/data/auth/oauth2_token_introspect.py rename to src/globus_sdk/testing/data/auth/oauth2_token_introspect.py index e05749c59..5927b6883 100644 --- a/src/globus_sdk/_testing/data/auth/oauth2_token_introspect.py +++ b/src/globus_sdk/testing/data/auth/oauth2_token_introspect.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet _kingfish = { "username": "kingfish@globus.org", diff --git a/src/globus_sdk/_testing/data/auth/oauth2_userinfo.py b/src/globus_sdk/testing/data/auth/oauth2_userinfo.py similarity index 100% rename from src/globus_sdk/_testing/data/auth/oauth2_userinfo.py rename to src/globus_sdk/testing/data/auth/oauth2_userinfo.py diff --git a/src/globus_sdk/_testing/data/auth/update_client.py b/src/globus_sdk/testing/data/auth/update_client.py similarity index 97% rename from src/globus_sdk/_testing/data/auth/update_client.py rename to src/globus_sdk/testing/data/auth/update_client.py index c122eed1f..7fa7c203a 100644 --- a/src/globus_sdk/_testing/data/auth/update_client.py +++ b/src/globus_sdk/testing/data/auth/update_client.py @@ -3,7 +3,7 @@ from responses.matchers import json_params_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet _COMMON_RESPONSE_RECORD = { "fqdns": [], diff --git a/src/globus_sdk/_testing/data/auth/update_policy.py b/src/globus_sdk/testing/data/auth/update_policy.py similarity index 98% rename from src/globus_sdk/_testing/data/auth/update_policy.py rename to src/globus_sdk/testing/data/auth/update_policy.py index 83925d73d..e198383be 100644 --- a/src/globus_sdk/_testing/data/auth/update_policy.py +++ b/src/globus_sdk/testing/data/auth/update_policy.py @@ -3,7 +3,7 @@ from responses.matchers import json_params_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet POLICY_REQUEST_ARGS = { "policy_id": str(uuid.uuid1()), diff --git a/src/globus_sdk/_testing/data/auth/update_project.py b/src/globus_sdk/testing/data/auth/update_project.py similarity index 96% rename from src/globus_sdk/_testing/data/auth/update_project.py rename to src/globus_sdk/testing/data/auth/update_project.py index c32959a30..67a3f5cff 100644 --- a/src/globus_sdk/_testing/data/auth/update_project.py +++ b/src/globus_sdk/testing/data/auth/update_project.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet project_id = str(uuid.uuid1()) star_lord = { diff --git a/src/globus_sdk/_testing/data/auth/update_scope.py b/src/globus_sdk/testing/data/auth/update_scope.py similarity index 98% rename from src/globus_sdk/_testing/data/auth/update_scope.py rename to src/globus_sdk/testing/data/auth/update_scope.py index 3d5555013..4bb62ea49 100644 --- a/src/globus_sdk/_testing/data/auth/update_scope.py +++ b/src/globus_sdk/testing/data/auth/update_scope.py @@ -4,7 +4,7 @@ from responses.matchers import json_params_matcher from globus_sdk import DependentScopeSpec -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet SCOPE_REQUEST_ARGS = { "scope_id": str(uuid.uuid1()), diff --git a/src/globus_sdk/_testing/data/auth/userinfo.py b/src/globus_sdk/testing/data/auth/userinfo.py similarity index 91% rename from src/globus_sdk/_testing/data/auth/userinfo.py rename to src/globus_sdk/testing/data/auth/userinfo.py index 2a72fb918..fe3f6adc8 100644 --- a/src/globus_sdk/_testing/data/auth/userinfo.py +++ b/src/globus_sdk/testing/data/auth/userinfo.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import FORBIDDEN_AUTH_RESPONSE, UNAUTHORIZED_AUTH_RESPONSE diff --git a/src/globus_sdk/_testing/data/compute/__init__.py b/src/globus_sdk/testing/data/compute/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/compute/__init__.py rename to src/globus_sdk/testing/data/compute/__init__.py diff --git a/src/globus_sdk/_testing/data/compute/_common.py b/src/globus_sdk/testing/data/compute/_common.py similarity index 100% rename from src/globus_sdk/_testing/data/compute/_common.py rename to src/globus_sdk/testing/data/compute/_common.py diff --git a/src/globus_sdk/_testing/data/compute/v2/__init__.py b/src/globus_sdk/testing/data/compute/v2/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/compute/v2/__init__.py rename to src/globus_sdk/testing/data/compute/v2/__init__.py diff --git a/src/globus_sdk/_testing/data/compute/v2/delete_endpoint.py b/src/globus_sdk/testing/data/compute/v2/delete_endpoint.py similarity index 79% rename from src/globus_sdk/_testing/data/compute/v2/delete_endpoint.py rename to src/globus_sdk/testing/data/compute/v2/delete_endpoint.py index e297ebc1d..fda19b179 100644 --- a/src/globus_sdk/_testing/data/compute/v2/delete_endpoint.py +++ b/src/globus_sdk/testing/data/compute/v2/delete_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/delete_function.py b/src/globus_sdk/testing/data/compute/v2/delete_function.py similarity index 79% rename from src/globus_sdk/_testing/data/compute/v2/delete_function.py rename to src/globus_sdk/testing/data/compute/v2/delete_function.py index 92a65c4b4..ef327c0f5 100644 --- a/src/globus_sdk/_testing/data/compute/v2/delete_function.py +++ b/src/globus_sdk/testing/data/compute/v2/delete_function.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import FUNCTION_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/get_endpoint.py b/src/globus_sdk/testing/data/compute/v2/get_endpoint.py similarity index 92% rename from src/globus_sdk/_testing/data/compute/v2/get_endpoint.py rename to src/globus_sdk/testing/data/compute/v2/get_endpoint.py index 8895fb25d..938224bc7 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_endpoint.py +++ b/src/globus_sdk/testing/data/compute/v2/get_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID, SUBSCRIPTION_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/get_endpoint_status.py b/src/globus_sdk/testing/data/compute/v2/get_endpoint_status.py similarity index 87% rename from src/globus_sdk/_testing/data/compute/v2/get_endpoint_status.py rename to src/globus_sdk/testing/data/compute/v2/get_endpoint_status.py index 551b32d83..2307e1af8 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_endpoint_status.py +++ b/src/globus_sdk/testing/data/compute/v2/get_endpoint_status.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/get_endpoints.py b/src/globus_sdk/testing/data/compute/v2/get_endpoints.py similarity index 95% rename from src/globus_sdk/_testing/data/compute/v2/get_endpoints.py rename to src/globus_sdk/testing/data/compute/v2/get_endpoints.py index 16c3f622a..cbf934a5b 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_endpoints.py +++ b/src/globus_sdk/testing/data/compute/v2/get_endpoints.py @@ -1,6 +1,6 @@ from responses.matchers import query_param_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID, ENDPOINT_ID_2, ENDPOINT_ID_3, NON_USER_ID, USER_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/get_function.py b/src/globus_sdk/testing/data/compute/v2/get_function.py similarity index 89% rename from src/globus_sdk/_testing/data/compute/v2/get_function.py rename to src/globus_sdk/testing/data/compute/v2/get_function.py index 21339526e..c55bda46c 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_function.py +++ b/src/globus_sdk/testing/data/compute/v2/get_function.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import FUNCTION_CODE, FUNCTION_ID, FUNCTION_NAME diff --git a/src/globus_sdk/_testing/data/compute/v2/get_result_amqp_url.py b/src/globus_sdk/testing/data/compute/v2/get_result_amqp_url.py similarity index 81% rename from src/globus_sdk/_testing/data/compute/v2/get_result_amqp_url.py rename to src/globus_sdk/testing/data/compute/v2/get_result_amqp_url.py index db8cafaa0..9e4734d40 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_result_amqp_url.py +++ b/src/globus_sdk/testing/data/compute/v2/get_result_amqp_url.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet DEFAULT_RESPONSE_DOC = { "queue_prefix": "some_prefix", diff --git a/src/globus_sdk/_testing/data/compute/v2/get_task.py b/src/globus_sdk/testing/data/compute/v2/get_task.py similarity index 78% rename from src/globus_sdk/_testing/data/compute/v2/get_task.py rename to src/globus_sdk/testing/data/compute/v2/get_task.py index 933be971f..c877b553e 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_task.py +++ b/src/globus_sdk/testing/data/compute/v2/get_task.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import TASK_DOC, TASK_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/get_task_batch.py b/src/globus_sdk/testing/data/compute/v2/get_task_batch.py similarity index 87% rename from src/globus_sdk/_testing/data/compute/v2/get_task_batch.py rename to src/globus_sdk/testing/data/compute/v2/get_task_batch.py index 451c2e8ba..7fb485867 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_task_batch.py +++ b/src/globus_sdk/testing/data/compute/v2/get_task_batch.py @@ -1,6 +1,6 @@ from responses.matchers import json_params_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import TASK_DOC, TASK_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/get_task_group.py b/src/globus_sdk/testing/data/compute/v2/get_task_group.py similarity index 89% rename from src/globus_sdk/_testing/data/compute/v2/get_task_group.py rename to src/globus_sdk/testing/data/compute/v2/get_task_group.py index 058687cd2..90e848887 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_task_group.py +++ b/src/globus_sdk/testing/data/compute/v2/get_task_group.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import TASK_GROUP_ID, TASK_ID, TASK_ID_2 diff --git a/src/globus_sdk/_testing/data/compute/v2/get_version.py b/src/globus_sdk/testing/data/compute/v2/get_version.py similarity index 90% rename from src/globus_sdk/_testing/data/compute/v2/get_version.py rename to src/globus_sdk/testing/data/compute/v2/get_version.py index 6706de913..3906a1f08 100644 --- a/src/globus_sdk/_testing/data/compute/v2/get_version.py +++ b/src/globus_sdk/testing/data/compute/v2/get_version.py @@ -1,6 +1,6 @@ from responses.matchers import query_param_matcher -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet API_VERSION = "1.23.0" ALL_RESPONSE_DOC = { diff --git a/src/globus_sdk/_testing/data/compute/v2/lock_endpoint.py b/src/globus_sdk/testing/data/compute/v2/lock_endpoint.py similarity index 85% rename from src/globus_sdk/_testing/data/compute/v2/lock_endpoint.py rename to src/globus_sdk/testing/data/compute/v2/lock_endpoint.py index acb12e33d..b22513778 100644 --- a/src/globus_sdk/_testing/data/compute/v2/lock_endpoint.py +++ b/src/globus_sdk/testing/data/compute/v2/lock_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/register_endpoint.py b/src/globus_sdk/testing/data/compute/v2/register_endpoint.py similarity index 90% rename from src/globus_sdk/_testing/data/compute/v2/register_endpoint.py rename to src/globus_sdk/testing/data/compute/v2/register_endpoint.py index a6d73dc28..8cf66b460 100644 --- a/src/globus_sdk/_testing/data/compute/v2/register_endpoint.py +++ b/src/globus_sdk/testing/data/compute/v2/register_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/compute/v2/register_function.py b/src/globus_sdk/testing/data/compute/v2/register_function.py similarity index 84% rename from src/globus_sdk/_testing/data/compute/v2/register_function.py rename to src/globus_sdk/testing/data/compute/v2/register_function.py index b0c68bd72..875ebea07 100644 --- a/src/globus_sdk/_testing/data/compute/v2/register_function.py +++ b/src/globus_sdk/testing/data/compute/v2/register_function.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import FUNCTION_CODE, FUNCTION_ID, FUNCTION_NAME diff --git a/src/globus_sdk/_testing/data/compute/v2/submit.py b/src/globus_sdk/testing/data/compute/v2/submit.py similarity index 91% rename from src/globus_sdk/_testing/data/compute/v2/submit.py rename to src/globus_sdk/testing/data/compute/v2/submit.py index 457deb065..faf557520 100644 --- a/src/globus_sdk/_testing/data/compute/v2/submit.py +++ b/src/globus_sdk/testing/data/compute/v2/submit.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import TASK_GROUP_ID, TASK_ID, TASK_ID_2 diff --git a/src/globus_sdk/_testing/data/compute/v3/__init__.py b/src/globus_sdk/testing/data/compute/v3/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/compute/v3/__init__.py rename to src/globus_sdk/testing/data/compute/v3/__init__.py diff --git a/src/globus_sdk/_testing/data/compute/v3/get_endpoint_allowlist.py b/src/globus_sdk/testing/data/compute/v3/get_endpoint_allowlist.py similarity index 86% rename from src/globus_sdk/_testing/data/compute/v3/get_endpoint_allowlist.py rename to src/globus_sdk/testing/data/compute/v3/get_endpoint_allowlist.py index 50063d58f..05fdfca48 100644 --- a/src/globus_sdk/_testing/data/compute/v3/get_endpoint_allowlist.py +++ b/src/globus_sdk/testing/data/compute/v3/get_endpoint_allowlist.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID, FUNCTION_ID, FUNCTION_ID_2 diff --git a/src/globus_sdk/_testing/data/compute/v3/lock_endpoint.py b/src/globus_sdk/testing/data/compute/v3/lock_endpoint.py similarity index 85% rename from src/globus_sdk/_testing/data/compute/v3/lock_endpoint.py rename to src/globus_sdk/testing/data/compute/v3/lock_endpoint.py index 29a669e14..1cdc66e6d 100644 --- a/src/globus_sdk/_testing/data/compute/v3/lock_endpoint.py +++ b/src/globus_sdk/testing/data/compute/v3/lock_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/compute/v3/register_endpoint.py b/src/globus_sdk/testing/data/compute/v3/register_endpoint.py similarity index 89% rename from src/globus_sdk/_testing/data/compute/v3/register_endpoint.py rename to src/globus_sdk/testing/data/compute/v3/register_endpoint.py index 5e62f8147..f6e1acfb3 100644 --- a/src/globus_sdk/_testing/data/compute/v3/register_endpoint.py +++ b/src/globus_sdk/testing/data/compute/v3/register_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/compute/v3/register_function.py b/src/globus_sdk/testing/data/compute/v3/register_function.py similarity index 86% rename from src/globus_sdk/_testing/data/compute/v3/register_function.py rename to src/globus_sdk/testing/data/compute/v3/register_function.py index c79a4fa78..b0e323153 100644 --- a/src/globus_sdk/_testing/data/compute/v3/register_function.py +++ b/src/globus_sdk/testing/data/compute/v3/register_function.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import FUNCTION_CODE, FUNCTION_ID, FUNCTION_NAME diff --git a/src/globus_sdk/_testing/data/compute/v3/submit.py b/src/globus_sdk/testing/data/compute/v3/submit.py similarity index 91% rename from src/globus_sdk/_testing/data/compute/v3/submit.py rename to src/globus_sdk/testing/data/compute/v3/submit.py index 5402cd32d..96db426f6 100644 --- a/src/globus_sdk/_testing/data/compute/v3/submit.py +++ b/src/globus_sdk/testing/data/compute/v3/submit.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID, FUNCTION_ID, TASK_GROUP_ID, TASK_ID, TASK_ID_2 diff --git a/src/globus_sdk/_testing/data/compute/v3/update_endpoint.py b/src/globus_sdk/testing/data/compute/v3/update_endpoint.py similarity index 90% rename from src/globus_sdk/_testing/data/compute/v3/update_endpoint.py rename to src/globus_sdk/testing/data/compute/v3/update_endpoint.py index e18e1271e..cc76c1a41 100644 --- a/src/globus_sdk/_testing/data/compute/v3/update_endpoint.py +++ b/src/globus_sdk/testing/data/compute/v3/update_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from .._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/flows/__init__.py b/src/globus_sdk/testing/data/flows/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/flows/__init__.py rename to src/globus_sdk/testing/data/flows/__init__.py diff --git a/src/globus_sdk/_testing/data/flows/_common.py b/src/globus_sdk/testing/data/flows/_common.py similarity index 100% rename from src/globus_sdk/_testing/data/flows/_common.py rename to src/globus_sdk/testing/data/flows/_common.py diff --git a/src/globus_sdk/_testing/data/flows/cancel_run.py b/src/globus_sdk/testing/data/flows/cancel_run.py similarity index 91% rename from src/globus_sdk/_testing/data/flows/cancel_run.py rename to src/globus_sdk/testing/data/flows/cancel_run.py index be0b8d3f6..ea59338b6 100644 --- a/src/globus_sdk/_testing/data/flows/cancel_run.py +++ b/src/globus_sdk/testing/data/flows/cancel_run.py @@ -1,6 +1,6 @@ import copy -from globus_sdk._testing import RegisteredResponse, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_RUN diff --git a/src/globus_sdk/_testing/data/flows/create_flow.py b/src/globus_sdk/testing/data/flows/create_flow.py similarity index 96% rename from src/globus_sdk/_testing/data/flows/create_flow.py rename to src/globus_sdk/testing/data/flows/create_flow.py index 40b53c01d..40297c02d 100644 --- a/src/globus_sdk/_testing/data/flows/create_flow.py +++ b/src/globus_sdk/testing/data/flows/create_flow.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_FLOW_DOC diff --git a/src/globus_sdk/_testing/data/flows/delete_flow.py b/src/globus_sdk/testing/data/flows/delete_flow.py similarity index 95% rename from src/globus_sdk/_testing/data/flows/delete_flow.py rename to src/globus_sdk/testing/data/flows/delete_flow.py index 93f9071d3..d7d24772b 100644 --- a/src/globus_sdk/_testing/data/flows/delete_flow.py +++ b/src/globus_sdk/testing/data/flows/delete_flow.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_FLOW_DOC, TWO_HOP_TRANSFER_FLOW_ID diff --git a/src/globus_sdk/_testing/data/flows/delete_run.py b/src/globus_sdk/testing/data/flows/delete_run.py similarity index 94% rename from src/globus_sdk/_testing/data/flows/delete_run.py rename to src/globus_sdk/testing/data/flows/delete_run.py index c8d7f63b2..bce819a9f 100644 --- a/src/globus_sdk/_testing/data/flows/delete_run.py +++ b/src/globus_sdk/testing/data/flows/delete_run.py @@ -1,6 +1,6 @@ import copy -from globus_sdk._testing import RegisteredResponse, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_RUN diff --git a/src/globus_sdk/_testing/data/flows/get_flow.py b/src/globus_sdk/testing/data/flows/get_flow.py similarity index 84% rename from src/globus_sdk/_testing/data/flows/get_flow.py rename to src/globus_sdk/testing/data/flows/get_flow.py index 916a92470..a27e7276e 100644 --- a/src/globus_sdk/_testing/data/flows/get_flow.py +++ b/src/globus_sdk/testing/data/flows/get_flow.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_FLOW_DOC, TWO_HOP_TRANSFER_FLOW_ID diff --git a/src/globus_sdk/_testing/data/flows/get_run.py b/src/globus_sdk/testing/data/flows/get_run.py similarity index 92% rename from src/globus_sdk/_testing/data/flows/get_run.py rename to src/globus_sdk/testing/data/flows/get_run.py index ffe6b16b3..bb8e43216 100644 --- a/src/globus_sdk/_testing/data/flows/get_run.py +++ b/src/globus_sdk/testing/data/flows/get_run.py @@ -2,7 +2,7 @@ from responses.matchers import query_param_matcher -from globus_sdk._testing import RegisteredResponse, ResponseList, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseList, ResponseSet from ._common import FLOW_DESCRIPTION, RUN, RUN_ID diff --git a/src/globus_sdk/_testing/data/flows/get_run_definition.py b/src/globus_sdk/testing/data/flows/get_run_definition.py similarity index 86% rename from src/globus_sdk/_testing/data/flows/get_run_definition.py rename to src/globus_sdk/testing/data/flows/get_run_definition.py index 3692b8fac..78aec4424 100644 --- a/src/globus_sdk/_testing/data/flows/get_run_definition.py +++ b/src/globus_sdk/testing/data/flows/get_run_definition.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing import RegisteredResponse, ResponseList, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseList, ResponseSet from ._common import RUN_ID diff --git a/src/globus_sdk/_testing/data/flows/get_run_logs.py b/src/globus_sdk/testing/data/flows/get_run_logs.py similarity index 99% rename from src/globus_sdk/_testing/data/flows/get_run_logs.py rename to src/globus_sdk/testing/data/flows/get_run_logs.py index 0e67f3513..5fe302d3f 100644 --- a/src/globus_sdk/_testing/data/flows/get_run_logs.py +++ b/src/globus_sdk/testing/data/flows/get_run_logs.py @@ -1,6 +1,6 @@ from responses.matchers import query_param_matcher -from globus_sdk._testing import RegisteredResponse, ResponseList, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseList, ResponseSet RUN_ID = "cfdaf0a4-0931-40af-b974-b619ce69f401" OWNER_URN = "urn:globus:auth:identity:944cfbe8-60f8-474d-a634-a0c1ad543a54" diff --git a/src/globus_sdk/_testing/data/flows/list_flows.py b/src/globus_sdk/testing/data/flows/list_flows.py similarity index 98% rename from src/globus_sdk/_testing/data/flows/list_flows.py rename to src/globus_sdk/testing/data/flows/list_flows.py index b2b3caf15..97abb5a89 100644 --- a/src/globus_sdk/_testing/data/flows/list_flows.py +++ b/src/globus_sdk/testing/data/flows/list_flows.py @@ -6,7 +6,7 @@ from responses import matchers -from globus_sdk._testing.models import RegisteredResponse, ResponseList, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseList, ResponseSet from ._common import TWO_HOP_TRANSFER_FLOW_DOC, TWO_HOP_TRANSFER_FLOW_ID diff --git a/src/globus_sdk/_testing/data/flows/list_runs.py b/src/globus_sdk/testing/data/flows/list_runs.py similarity index 98% rename from src/globus_sdk/_testing/data/flows/list_runs.py rename to src/globus_sdk/testing/data/flows/list_runs.py index c967afe0c..ab060058b 100644 --- a/src/globus_sdk/_testing/data/flows/list_runs.py +++ b/src/globus_sdk/testing/data/flows/list_runs.py @@ -7,7 +7,7 @@ from responses import matchers -from globus_sdk._testing import RegisteredResponse, ResponseList, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseList, ResponseSet from ._common import RUN, RUN_ID, USER1 diff --git a/src/globus_sdk/_testing/data/flows/resume_run.py b/src/globus_sdk/testing/data/flows/resume_run.py similarity index 85% rename from src/globus_sdk/_testing/data/flows/resume_run.py rename to src/globus_sdk/testing/data/flows/resume_run.py index d06693248..5c0f11ee3 100644 --- a/src/globus_sdk/_testing/data/flows/resume_run.py +++ b/src/globus_sdk/testing/data/flows/resume_run.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import RegisteredResponse, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_RUN diff --git a/src/globus_sdk/_testing/data/flows/run_flow.py b/src/globus_sdk/testing/data/flows/run_flow.py similarity index 93% rename from src/globus_sdk/_testing/data/flows/run_flow.py rename to src/globus_sdk/testing/data/flows/run_flow.py index d3756993a..32b8fa382 100644 --- a/src/globus_sdk/_testing/data/flows/run_flow.py +++ b/src/globus_sdk/testing/data/flows/run_flow.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_FLOW_ID, TWO_HOP_TRANSFER_RUN diff --git a/src/globus_sdk/_testing/data/flows/update_flow.py b/src/globus_sdk/testing/data/flows/update_flow.py similarity index 92% rename from src/globus_sdk/_testing/data/flows/update_flow.py rename to src/globus_sdk/testing/data/flows/update_flow.py index 79e5087d3..3988b7be6 100644 --- a/src/globus_sdk/_testing/data/flows/update_flow.py +++ b/src/globus_sdk/testing/data/flows/update_flow.py @@ -1,6 +1,6 @@ from copy import deepcopy -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_FLOW_DOC, TWO_HOP_TRANSFER_FLOW_ID diff --git a/src/globus_sdk/_testing/data/flows/update_run.py b/src/globus_sdk/testing/data/flows/update_run.py similarity index 84% rename from src/globus_sdk/_testing/data/flows/update_run.py rename to src/globus_sdk/testing/data/flows/update_run.py index 716f5b807..ef69da007 100644 --- a/src/globus_sdk/_testing/data/flows/update_run.py +++ b/src/globus_sdk/testing/data/flows/update_run.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import RegisteredResponse, ResponseSet +from globus_sdk.testing import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_RUN diff --git a/src/globus_sdk/_testing/data/flows/validate_flow.py b/src/globus_sdk/testing/data/flows/validate_flow.py similarity index 97% rename from src/globus_sdk/_testing/data/flows/validate_flow.py rename to src/globus_sdk/testing/data/flows/validate_flow.py index 593533dfa..c50f9e769 100644 --- a/src/globus_sdk/_testing/data/flows/validate_flow.py +++ b/src/globus_sdk/testing/data/flows/validate_flow.py @@ -1,6 +1,6 @@ from responses import matchers -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet VALIDATE_SIMPLE_FLOW_DEFINITION = { "Comment": "Simple flow", diff --git a/src/globus_sdk/_testing/data/flows/validate_run.py b/src/globus_sdk/testing/data/flows/validate_run.py similarity index 97% rename from src/globus_sdk/_testing/data/flows/validate_run.py rename to src/globus_sdk/testing/data/flows/validate_run.py index 42e82a7ce..3a609a28c 100644 --- a/src/globus_sdk/_testing/data/flows/validate_run.py +++ b/src/globus_sdk/testing/data/flows/validate_run.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TWO_HOP_TRANSFER_FLOW_ID diff --git a/src/globus_sdk/_testing/data/globus_connect_server/__init__.py b/src/globus_sdk/testing/data/globus_connect_server/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/globus_connect_server/__init__.py rename to src/globus_sdk/testing/data/globus_connect_server/__init__.py diff --git a/src/globus_sdk/_testing/data/globus_connect_server/create_storage_gateway.py b/src/globus_sdk/testing/data/globus_connect_server/create_storage_gateway.py similarity index 96% rename from src/globus_sdk/_testing/data/globus_connect_server/create_storage_gateway.py rename to src/globus_sdk/testing/data/globus_connect_server/create_storage_gateway.py index e8df287a6..8feaa7625 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/create_storage_gateway.py +++ b/src/globus_sdk/testing/data/globus_connect_server/create_storage_gateway.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet metadata = { "id": "daa09846-eb92-11e9-b89c-9cb6d0d9fd63", diff --git a/src/globus_sdk/_testing/data/globus_connect_server/create_user_credential.py b/src/globus_sdk/testing/data/globus_connect_server/create_user_credential.py similarity index 94% rename from src/globus_sdk/_testing/data/globus_connect_server/create_user_credential.py rename to src/globus_sdk/testing/data/globus_connect_server/create_user_credential.py index cdacc1c3e..cac523b5a 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/create_user_credential.py +++ b/src/globus_sdk/testing/data/globus_connect_server/create_user_credential.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CREDENTIAL_ID = "af43d884-64a1-4414-897a-680c32374439" diff --git a/src/globus_sdk/_testing/data/globus_connect_server/delete_storage_gateway.py b/src/globus_sdk/testing/data/globus_connect_server/delete_storage_gateway.py similarity index 93% rename from src/globus_sdk/_testing/data/globus_connect_server/delete_storage_gateway.py rename to src/globus_sdk/testing/data/globus_connect_server/delete_storage_gateway.py index b1ee7ae38..62b11dc9f 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/delete_storage_gateway.py +++ b/src/globus_sdk/testing/data/globus_connect_server/delete_storage_gateway.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet metadata = { "id": "daa09846-eb92-11e9-b89c-9cb6d0d9fd63", diff --git a/src/globus_sdk/_testing/data/globus_connect_server/delete_user_credential.py b/src/globus_sdk/testing/data/globus_connect_server/delete_user_credential.py similarity index 88% rename from src/globus_sdk/_testing/data/globus_connect_server/delete_user_credential.py rename to src/globus_sdk/testing/data/globus_connect_server/delete_user_credential.py index 6a85ba8a6..4b82b5fb3 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/delete_user_credential.py +++ b/src/globus_sdk/testing/data/globus_connect_server/delete_user_credential.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CREDENTIAL_ID = "af43d884-64a1-4414-897a-680c32374439" diff --git a/src/globus_sdk/_testing/data/globus_connect_server/get_collection_list.py b/src/globus_sdk/testing/data/globus_connect_server/get_collection_list.py similarity index 97% rename from src/globus_sdk/_testing/data/globus_connect_server/get_collection_list.py rename to src/globus_sdk/testing/data/globus_connect_server/get_collection_list.py index 61dc6cf99..30fd6e651 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/get_collection_list.py +++ b/src/globus_sdk/testing/data/globus_connect_server/get_collection_list.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet identity_id = str(uuid.uuid4()) collection_ids = [str(uuid.uuid4()), str(uuid.uuid4())] diff --git a/src/globus_sdk/_testing/data/globus_connect_server/get_endpoint.py b/src/globus_sdk/testing/data/globus_connect_server/get_endpoint.py similarity index 94% rename from src/globus_sdk/_testing/data/globus_connect_server/get_endpoint.py rename to src/globus_sdk/testing/data/globus_connect_server/get_endpoint.py index 82f85e355..523fcea4d 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/get_endpoint.py +++ b/src/globus_sdk/testing/data/globus_connect_server/get_endpoint.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet endpoint_id = str(uuid.uuid4()) gcs_manager_url = RegisteredResponse._url_map["gcs"] diff --git a/src/globus_sdk/_testing/data/globus_connect_server/get_gcs_info.py b/src/globus_sdk/testing/data/globus_connect_server/get_gcs_info.py similarity index 93% rename from src/globus_sdk/_testing/data/globus_connect_server/get_gcs_info.py rename to src/globus_sdk/testing/data/globus_connect_server/get_gcs_info.py index a0d1a6022..f93d1a450 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/get_gcs_info.py +++ b/src/globus_sdk/testing/data/globus_connect_server/get_gcs_info.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet endpoint_client_id = str(uuid.uuid4()) domain_name = "abc.xyz.data.globus.org" diff --git a/src/globus_sdk/_testing/data/globus_connect_server/get_storage_gateway.py b/src/globus_sdk/testing/data/globus_connect_server/get_storage_gateway.py similarity index 95% rename from src/globus_sdk/_testing/data/globus_connect_server/get_storage_gateway.py rename to src/globus_sdk/testing/data/globus_connect_server/get_storage_gateway.py index 8374c27a7..9578e0bf5 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/get_storage_gateway.py +++ b/src/globus_sdk/testing/data/globus_connect_server/get_storage_gateway.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet metadata = { "id": "daa09846-eb92-11e9-b89c-9cb6d0d9fd63", diff --git a/src/globus_sdk/_testing/data/globus_connect_server/get_storage_gateway_list.py b/src/globus_sdk/testing/data/globus_connect_server/get_storage_gateway_list.py similarity index 97% rename from src/globus_sdk/_testing/data/globus_connect_server/get_storage_gateway_list.py rename to src/globus_sdk/testing/data/globus_connect_server/get_storage_gateway_list.py index c9c268b15..7ba16adfb 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/get_storage_gateway_list.py +++ b/src/globus_sdk/testing/data/globus_connect_server/get_storage_gateway_list.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet GATEWAY_IDS = [ "a0cbde58-0183-11ea-92bd-9cb6d0d9fd63", diff --git a/src/globus_sdk/_testing/data/globus_connect_server/get_user_credential.py b/src/globus_sdk/testing/data/globus_connect_server/get_user_credential.py similarity index 94% rename from src/globus_sdk/_testing/data/globus_connect_server/get_user_credential.py rename to src/globus_sdk/testing/data/globus_connect_server/get_user_credential.py index c13f925de..9dce5fbc4 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/get_user_credential.py +++ b/src/globus_sdk/testing/data/globus_connect_server/get_user_credential.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CREDENTIAL_ID = "af43d884-64a1-4414-897a-680c32374439" diff --git a/src/globus_sdk/_testing/data/globus_connect_server/get_user_credential_list.py b/src/globus_sdk/testing/data/globus_connect_server/get_user_credential_list.py similarity index 96% rename from src/globus_sdk/_testing/data/globus_connect_server/get_user_credential_list.py rename to src/globus_sdk/testing/data/globus_connect_server/get_user_credential_list.py index aeb594f50..cd951a913 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/get_user_credential_list.py +++ b/src/globus_sdk/testing/data/globus_connect_server/get_user_credential_list.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CREDENTIAL_IDS = [ "af43d884-64a1-4414-897a-680c32374439", diff --git a/src/globus_sdk/_testing/data/globus_connect_server/update_endpoint.py b/src/globus_sdk/testing/data/globus_connect_server/update_endpoint.py similarity index 95% rename from src/globus_sdk/_testing/data/globus_connect_server/update_endpoint.py rename to src/globus_sdk/testing/data/globus_connect_server/update_endpoint.py index 32ae0bffb..fc741130f 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/update_endpoint.py +++ b/src/globus_sdk/testing/data/globus_connect_server/update_endpoint.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet endpoint_id = str(uuid.uuid4()) gcs_manager_url = RegisteredResponse._url_map["gcs"] diff --git a/src/globus_sdk/_testing/data/globus_connect_server/update_storage_gateway.py b/src/globus_sdk/testing/data/globus_connect_server/update_storage_gateway.py similarity index 95% rename from src/globus_sdk/_testing/data/globus_connect_server/update_storage_gateway.py rename to src/globus_sdk/testing/data/globus_connect_server/update_storage_gateway.py index d43b84bf7..d6c34c3ad 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/update_storage_gateway.py +++ b/src/globus_sdk/testing/data/globus_connect_server/update_storage_gateway.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet metadata = { "id": "daa09846-eb92-11e9-b89c-9cb6d0d9fd63", diff --git a/src/globus_sdk/_testing/data/globus_connect_server/update_user_credential.py b/src/globus_sdk/testing/data/globus_connect_server/update_user_credential.py similarity index 94% rename from src/globus_sdk/_testing/data/globus_connect_server/update_user_credential.py rename to src/globus_sdk/testing/data/globus_connect_server/update_user_credential.py index f31551138..d048c86ff 100644 --- a/src/globus_sdk/_testing/data/globus_connect_server/update_user_credential.py +++ b/src/globus_sdk/testing/data/globus_connect_server/update_user_credential.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet CREDENTIAL_ID = "af43d884-64a1-4414-897a-680c32374439" diff --git a/src/globus_sdk/_testing/data/groups/__init__.py b/src/globus_sdk/testing/data/groups/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/groups/__init__.py rename to src/globus_sdk/testing/data/groups/__init__.py diff --git a/src/globus_sdk/_testing/data/groups/_common.py b/src/globus_sdk/testing/data/groups/_common.py similarity index 100% rename from src/globus_sdk/_testing/data/groups/_common.py rename to src/globus_sdk/testing/data/groups/_common.py diff --git a/src/globus_sdk/_testing/data/groups/create_group.py b/src/globus_sdk/testing/data/groups/create_group.py similarity index 78% rename from src/globus_sdk/_testing/data/groups/create_group.py rename to src/globus_sdk/testing/data/groups/create_group.py index 513ba0888..083bd4a7a 100644 --- a/src/globus_sdk/_testing/data/groups/create_group.py +++ b/src/globus_sdk/testing/data/groups/create_group.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import BASE_GROUP_DOC, GROUP_ID diff --git a/src/globus_sdk/_testing/data/groups/delete_group.py b/src/globus_sdk/testing/data/groups/delete_group.py similarity index 79% rename from src/globus_sdk/_testing/data/groups/delete_group.py rename to src/globus_sdk/testing/data/groups/delete_group.py index a055ede33..2d748cd4a 100644 --- a/src/globus_sdk/_testing/data/groups/delete_group.py +++ b/src/globus_sdk/testing/data/groups/delete_group.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import BASE_GROUP_DOC, GROUP_ID diff --git a/src/globus_sdk/_testing/data/groups/get_group.py b/src/globus_sdk/testing/data/groups/get_group.py similarity index 89% rename from src/globus_sdk/_testing/data/groups/get_group.py rename to src/globus_sdk/testing/data/groups/get_group.py index 4ff0635ee..81a747011 100644 --- a/src/globus_sdk/_testing/data/groups/get_group.py +++ b/src/globus_sdk/testing/data/groups/get_group.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ( BASE_GROUP_DOC, diff --git a/src/globus_sdk/_testing/data/groups/get_group_by_subscription_id.py b/src/globus_sdk/testing/data/groups/get_group_by_subscription_id.py similarity index 90% rename from src/globus_sdk/_testing/data/groups/get_group_by_subscription_id.py rename to src/globus_sdk/testing/data/groups/get_group_by_subscription_id.py index 53ab43215..f44e80875 100644 --- a/src/globus_sdk/_testing/data/groups/get_group_by_subscription_id.py +++ b/src/globus_sdk/testing/data/groups/get_group_by_subscription_id.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import SUBSCRIPTION_GROUP_ID, SUBSCRIPTION_ID, SUBSCRIPTION_INFO diff --git a/src/globus_sdk/_testing/data/groups/get_my_groups.py b/src/globus_sdk/testing/data/groups/get_my_groups.py similarity index 98% rename from src/globus_sdk/_testing/data/groups/get_my_groups.py rename to src/globus_sdk/testing/data/groups/get_my_groups.py index 73523d28e..670989665 100644 --- a/src/globus_sdk/_testing/data/groups/get_my_groups.py +++ b/src/globus_sdk/testing/data/groups/get_my_groups.py @@ -2,7 +2,7 @@ import typing as t -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet raw_data: list[dict[str, t.Any]] = [ { diff --git a/src/globus_sdk/_testing/data/groups/set_group_policies.py b/src/globus_sdk/testing/data/groups/set_group_policies.py similarity index 88% rename from src/globus_sdk/_testing/data/groups/set_group_policies.py rename to src/globus_sdk/testing/data/groups/set_group_policies.py index 59b8970d5..22cc78164 100644 --- a/src/globus_sdk/_testing/data/groups/set_group_policies.py +++ b/src/globus_sdk/testing/data/groups/set_group_policies.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import GROUP_ID diff --git a/src/globus_sdk/_testing/data/search/__init__.py b/src/globus_sdk/testing/data/search/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/search/__init__.py rename to src/globus_sdk/testing/data/search/__init__.py diff --git a/src/globus_sdk/_testing/data/search/batch_delete_by_subject.py b/src/globus_sdk/testing/data/search/batch_delete_by_subject.py similarity index 82% rename from src/globus_sdk/_testing/data/search/batch_delete_by_subject.py rename to src/globus_sdk/testing/data/search/batch_delete_by_subject.py index d66423d26..acdca126f 100644 --- a/src/globus_sdk/_testing/data/search/batch_delete_by_subject.py +++ b/src/globus_sdk/testing/data/search/batch_delete_by_subject.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = str(uuid.uuid1()) TASK_ID = str(uuid.uuid1()) diff --git a/src/globus_sdk/_testing/data/search/create_index.py b/src/globus_sdk/testing/data/search/create_index.py similarity index 95% rename from src/globus_sdk/_testing/data/search/create_index.py rename to src/globus_sdk/testing/data/search/create_index.py index 428677bb0..224edc8cc 100644 --- a/src/globus_sdk/_testing/data/search/create_index.py +++ b/src/globus_sdk/testing/data/search/create_index.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = str(uuid.uuid4()) diff --git a/src/globus_sdk/_testing/data/search/create_role.py b/src/globus_sdk/testing/data/search/create_role.py similarity index 92% rename from src/globus_sdk/_testing/data/search/create_role.py rename to src/globus_sdk/testing/data/search/create_role.py index 41f5dfeee..e148dd065 100644 --- a/src/globus_sdk/_testing/data/search/create_role.py +++ b/src/globus_sdk/testing/data/search/create_role.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = "60d1160b-f016-40b0-8545-99619865873d" IDENTITY_ID = "46bd0f56-e24f-11e5-a510-131bef46955c" diff --git a/src/globus_sdk/_testing/data/search/delete_index.py b/src/globus_sdk/testing/data/search/delete_index.py similarity index 92% rename from src/globus_sdk/_testing/data/search/delete_index.py rename to src/globus_sdk/testing/data/search/delete_index.py index 762fe3d53..697923837 100644 --- a/src/globus_sdk/_testing/data/search/delete_index.py +++ b/src/globus_sdk/testing/data/search/delete_index.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = str(uuid.uuid4()) diff --git a/src/globus_sdk/_testing/data/search/delete_role.py b/src/globus_sdk/testing/data/search/delete_role.py similarity index 91% rename from src/globus_sdk/_testing/data/search/delete_role.py rename to src/globus_sdk/testing/data/search/delete_role.py index 6f5832ea0..591aa7df9 100644 --- a/src/globus_sdk/_testing/data/search/delete_role.py +++ b/src/globus_sdk/testing/data/search/delete_role.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = "60d1160b-f016-40b0-8545-99619865873d" IDENTITY_ID = "46bd0f56-e24f-11e5-a510-131bef46955c" diff --git a/src/globus_sdk/_testing/data/search/get_role_list.py b/src/globus_sdk/testing/data/search/get_role_list.py similarity index 94% rename from src/globus_sdk/_testing/data/search/get_role_list.py rename to src/globus_sdk/testing/data/search/get_role_list.py index b7e12f4d8..77aa041d0 100644 --- a/src/globus_sdk/_testing/data/search/get_role_list.py +++ b/src/globus_sdk/testing/data/search/get_role_list.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = "60d1160b-f016-40b0-8545-99619865873d" IDENTITY_IDS = [ diff --git a/src/globus_sdk/_testing/data/search/index_list.py b/src/globus_sdk/testing/data/search/index_list.py similarity index 95% rename from src/globus_sdk/_testing/data/search/index_list.py rename to src/globus_sdk/testing/data/search/index_list.py index d7bc3e40c..85214582f 100644 --- a/src/globus_sdk/_testing/data/search/index_list.py +++ b/src/globus_sdk/testing/data/search/index_list.py @@ -3,7 +3,7 @@ import typing as t import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_IDS = [str(uuid.uuid1()), str(uuid.uuid1())] INDEX_ATTRIBUTES: dict[str, dict[str, t.Any]] = { diff --git a/src/globus_sdk/_testing/data/search/post_search.py b/src/globus_sdk/testing/data/search/post_search.py similarity index 92% rename from src/globus_sdk/_testing/data/search/post_search.py rename to src/globus_sdk/testing/data/search/post_search.py index 0cc56b4d5..3a5cfc9f4 100644 --- a/src/globus_sdk/_testing/data/search/post_search.py +++ b/src/globus_sdk/testing/data/search/post_search.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = "60d1160b-f016-40b0-8545-99619865873d" diff --git a/src/globus_sdk/_testing/data/search/reopen_index.py b/src/globus_sdk/testing/data/search/reopen_index.py similarity index 93% rename from src/globus_sdk/_testing/data/search/reopen_index.py rename to src/globus_sdk/testing/data/search/reopen_index.py index 069d67806..2f8069b3a 100644 --- a/src/globus_sdk/_testing/data/search/reopen_index.py +++ b/src/globus_sdk/testing/data/search/reopen_index.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = str(uuid.uuid4()) diff --git a/src/globus_sdk/_testing/data/search/search.py b/src/globus_sdk/testing/data/search/search.py similarity index 92% rename from src/globus_sdk/_testing/data/search/search.py rename to src/globus_sdk/testing/data/search/search.py index f2239532f..5ef1db55a 100644 --- a/src/globus_sdk/_testing/data/search/search.py +++ b/src/globus_sdk/testing/data/search/search.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = "60d1160b-f016-40b0-8545-99619865873d" diff --git a/src/globus_sdk/_testing/data/timer/__init__.py b/src/globus_sdk/testing/data/timer/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/timer/__init__.py rename to src/globus_sdk/testing/data/timer/__init__.py diff --git a/src/globus_sdk/_testing/data/timer/_common.py b/src/globus_sdk/testing/data/timer/_common.py similarity index 100% rename from src/globus_sdk/_testing/data/timer/_common.py rename to src/globus_sdk/testing/data/timer/_common.py diff --git a/src/globus_sdk/_testing/data/timer/create_job.py b/src/globus_sdk/testing/data/timer/create_job.py similarity index 93% rename from src/globus_sdk/_testing/data/timer/create_job.py rename to src/globus_sdk/testing/data/timer/create_job.py index bad50aea4..1a8e54867 100644 --- a/src/globus_sdk/_testing/data/timer/create_job.py +++ b/src/globus_sdk/testing/data/timer/create_job.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TIMER_ID, V1_TIMER diff --git a/src/globus_sdk/_testing/data/timer/create_timer.py b/src/globus_sdk/testing/data/timer/create_timer.py similarity index 86% rename from src/globus_sdk/_testing/data/timer/create_timer.py rename to src/globus_sdk/testing/data/timer/create_timer.py index 18a6b234c..9cdc86e2d 100644 --- a/src/globus_sdk/_testing/data/timer/create_timer.py +++ b/src/globus_sdk/testing/data/timer/create_timer.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import DEST_EP_ID, SOURCE_EP_ID, TIMER_ID, V2_TRANSFER_TIMER diff --git a/src/globus_sdk/_testing/data/timer/delete_job.py b/src/globus_sdk/testing/data/timer/delete_job.py similarity index 77% rename from src/globus_sdk/_testing/data/timer/delete_job.py rename to src/globus_sdk/testing/data/timer/delete_job.py index 3288f1bcb..f501b56f2 100644 --- a/src/globus_sdk/_testing/data/timer/delete_job.py +++ b/src/globus_sdk/testing/data/timer/delete_job.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TIMER_ID, V1_TIMER diff --git a/src/globus_sdk/_testing/data/timer/get_job.py b/src/globus_sdk/testing/data/timer/get_job.py similarity index 96% rename from src/globus_sdk/_testing/data/timer/get_job.py rename to src/globus_sdk/testing/data/timer/get_job.py index 0ccb489ee..fa587bfc9 100644 --- a/src/globus_sdk/_testing/data/timer/get_job.py +++ b/src/globus_sdk/testing/data/timer/get_job.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TIMER_ID, V1_TIMER diff --git a/src/globus_sdk/_testing/data/timer/list_jobs.py b/src/globus_sdk/testing/data/timer/list_jobs.py similarity index 77% rename from src/globus_sdk/_testing/data/timer/list_jobs.py rename to src/globus_sdk/testing/data/timer/list_jobs.py index ce9014465..18d9f3781 100644 --- a/src/globus_sdk/_testing/data/timer/list_jobs.py +++ b/src/globus_sdk/testing/data/timer/list_jobs.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TIMER_ID, V1_TIMER diff --git a/src/globus_sdk/_testing/data/timer/pause_job.py b/src/globus_sdk/testing/data/timer/pause_job.py similarity index 80% rename from src/globus_sdk/_testing/data/timer/pause_job.py rename to src/globus_sdk/testing/data/timer/pause_job.py index cb72ccb46..79e2c61a8 100644 --- a/src/globus_sdk/_testing/data/timer/pause_job.py +++ b/src/globus_sdk/testing/data/timer/pause_job.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TIMER_ID diff --git a/src/globus_sdk/_testing/data/timer/resume_job.py b/src/globus_sdk/testing/data/timer/resume_job.py similarity index 80% rename from src/globus_sdk/_testing/data/timer/resume_job.py rename to src/globus_sdk/testing/data/timer/resume_job.py index 971d863a6..51056e6d2 100644 --- a/src/globus_sdk/_testing/data/timer/resume_job.py +++ b/src/globus_sdk/testing/data/timer/resume_job.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TIMER_ID diff --git a/src/globus_sdk/_testing/data/timer/update_job.py b/src/globus_sdk/testing/data/timer/update_job.py similarity index 85% rename from src/globus_sdk/_testing/data/timer/update_job.py rename to src/globus_sdk/testing/data/timer/update_job.py index 0be527a80..ad89200b5 100644 --- a/src/globus_sdk/_testing/data/timer/update_job.py +++ b/src/globus_sdk/testing/data/timer/update_job.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TIMER_ID, V1_TIMER diff --git a/src/globus_sdk/_testing/data/transfer/__init__.py b/src/globus_sdk/testing/data/transfer/__init__.py similarity index 100% rename from src/globus_sdk/_testing/data/transfer/__init__.py rename to src/globus_sdk/testing/data/transfer/__init__.py diff --git a/src/globus_sdk/_testing/data/transfer/_common.py b/src/globus_sdk/testing/data/transfer/_common.py similarity index 100% rename from src/globus_sdk/_testing/data/transfer/_common.py rename to src/globus_sdk/testing/data/transfer/_common.py diff --git a/src/globus_sdk/_testing/data/transfer/create_endpoint.py b/src/globus_sdk/testing/data/transfer/create_endpoint.py similarity index 89% rename from src/globus_sdk/_testing/data/transfer/create_endpoint.py rename to src/globus_sdk/testing/data/transfer/create_endpoint.py index 7a5d9ce8e..4b8fa534a 100644 --- a/src/globus_sdk/_testing/data/transfer/create_endpoint.py +++ b/src/globus_sdk/testing/data/transfer/create_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_list.py b/src/globus_sdk/testing/data/transfer/endpoint_manager_task_list.py similarity index 98% rename from src/globus_sdk/_testing/data/transfer/endpoint_manager_task_list.py rename to src/globus_sdk/testing/data/transfer/endpoint_manager_task_list.py index 9b1897aed..49c41dc8d 100644 --- a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_list.py +++ b/src/globus_sdk/testing/data/transfer/endpoint_manager_task_list.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID as SRC_ENDPOINT_ID from ._common import TASK_ID diff --git a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_successful_transfers.py b/src/globus_sdk/testing/data/transfer/endpoint_manager_task_successful_transfers.py similarity index 90% rename from src/globus_sdk/_testing/data/transfer/endpoint_manager_task_successful_transfers.py rename to src/globus_sdk/testing/data/transfer/endpoint_manager_task_successful_transfers.py index b81c7b969..7d5af62a2 100644 --- a/src/globus_sdk/_testing/data/transfer/endpoint_manager_task_successful_transfers.py +++ b/src/globus_sdk/testing/data/transfer/endpoint_manager_task_successful_transfers.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import TASK_ID diff --git a/src/globus_sdk/_testing/data/transfer/get_endpoint.py b/src/globus_sdk/testing/data/transfer/get_endpoint.py similarity index 95% rename from src/globus_sdk/_testing/data/transfer/get_endpoint.py rename to src/globus_sdk/testing/data/transfer/get_endpoint.py index b0112132b..13e18cedf 100644 --- a/src/globus_sdk/_testing/data/transfer/get_endpoint.py +++ b/src/globus_sdk/testing/data/transfer/get_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/transfer/get_submission_id.py b/src/globus_sdk/testing/data/transfer/get_submission_id.py similarity index 78% rename from src/globus_sdk/_testing/data/transfer/get_submission_id.py rename to src/globus_sdk/testing/data/transfer/get_submission_id.py index 077dac261..4a4eb925c 100644 --- a/src/globus_sdk/_testing/data/transfer/get_submission_id.py +++ b/src/globus_sdk/testing/data/transfer/get_submission_id.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import SUBMISSION_ID diff --git a/src/globus_sdk/_testing/data/transfer/operation_mkdir.py b/src/globus_sdk/testing/data/transfer/operation_mkdir.py similarity index 88% rename from src/globus_sdk/_testing/data/transfer/operation_mkdir.py rename to src/globus_sdk/testing/data/transfer/operation_mkdir.py index aa4beb5c6..0e64fd036 100644 --- a/src/globus_sdk/_testing/data/transfer/operation_mkdir.py +++ b/src/globus_sdk/testing/data/transfer/operation_mkdir.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/transfer/operation_rename.py b/src/globus_sdk/testing/data/transfer/operation_rename.py similarity index 88% rename from src/globus_sdk/_testing/data/transfer/operation_rename.py rename to src/globus_sdk/testing/data/transfer/operation_rename.py index 9db38edb4..d49b5790d 100644 --- a/src/globus_sdk/_testing/data/transfer/operation_rename.py +++ b/src/globus_sdk/testing/data/transfer/operation_rename.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/transfer/operation_stat.py b/src/globus_sdk/testing/data/transfer/operation_stat.py similarity index 96% rename from src/globus_sdk/_testing/data/transfer/operation_stat.py rename to src/globus_sdk/testing/data/transfer/operation_stat.py index a59f72a84..4121557eb 100644 --- a/src/globus_sdk/_testing/data/transfer/operation_stat.py +++ b/src/globus_sdk/testing/data/transfer/operation_stat.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/data/transfer/set_subscription_admin_verified.py b/src/globus_sdk/testing/data/transfer/set_subscription_admin_verified.py similarity index 97% rename from src/globus_sdk/_testing/data/transfer/set_subscription_admin_verified.py rename to src/globus_sdk/testing/data/transfer/set_subscription_admin_verified.py index 3a557a4ed..45fd3749c 100644 --- a/src/globus_sdk/_testing/data/transfer/set_subscription_admin_verified.py +++ b/src/globus_sdk/testing/data/transfer/set_subscription_admin_verified.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID, SUBSCRIPTION_ID diff --git a/src/globus_sdk/_testing/data/transfer/set_subscription_id.py b/src/globus_sdk/testing/data/transfer/set_subscription_id.py similarity index 96% rename from src/globus_sdk/_testing/data/transfer/set_subscription_id.py rename to src/globus_sdk/testing/data/transfer/set_subscription_id.py index d829cea84..ac087a641 100644 --- a/src/globus_sdk/_testing/data/transfer/set_subscription_id.py +++ b/src/globus_sdk/testing/data/transfer/set_subscription_id.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID, SUBSCRIPTION_ID diff --git a/src/globus_sdk/_testing/data/transfer/submit_delete.py b/src/globus_sdk/testing/data/transfer/submit_delete.py similarity index 92% rename from src/globus_sdk/_testing/data/transfer/submit_delete.py rename to src/globus_sdk/testing/data/transfer/submit_delete.py index 5ffa6b7c9..ba682034c 100644 --- a/src/globus_sdk/_testing/data/transfer/submit_delete.py +++ b/src/globus_sdk/testing/data/transfer/submit_delete.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import SUBMISSION_ID, TASK_ID diff --git a/src/globus_sdk/_testing/data/transfer/submit_transfer.py b/src/globus_sdk/testing/data/transfer/submit_transfer.py similarity index 95% rename from src/globus_sdk/_testing/data/transfer/submit_transfer.py rename to src/globus_sdk/testing/data/transfer/submit_transfer.py index a76d1878b..f204aa1a0 100644 --- a/src/globus_sdk/_testing/data/transfer/submit_transfer.py +++ b/src/globus_sdk/testing/data/transfer/submit_transfer.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import SUBMISSION_ID, TASK_ID diff --git a/src/globus_sdk/_testing/data/transfer/task_list.py b/src/globus_sdk/testing/data/transfer/task_list.py similarity index 97% rename from src/globus_sdk/_testing/data/transfer/task_list.py rename to src/globus_sdk/testing/data/transfer/task_list.py index 0903fe748..76b115c38 100644 --- a/src/globus_sdk/_testing/data/transfer/task_list.py +++ b/src/globus_sdk/testing/data/transfer/task_list.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet source_id = str(uuid.uuid4()) destination_id = str(uuid.uuid4()) diff --git a/src/globus_sdk/_testing/data/transfer/update_endpoint.py b/src/globus_sdk/testing/data/transfer/update_endpoint.py similarity index 87% rename from src/globus_sdk/_testing/data/transfer/update_endpoint.py rename to src/globus_sdk/testing/data/transfer/update_endpoint.py index 82220d6b7..a3185a61a 100644 --- a/src/globus_sdk/_testing/data/transfer/update_endpoint.py +++ b/src/globus_sdk/testing/data/transfer/update_endpoint.py @@ -1,4 +1,4 @@ -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet from ._common import ENDPOINT_ID diff --git a/src/globus_sdk/_testing/helpers.py b/src/globus_sdk/testing/helpers.py similarity index 100% rename from src/globus_sdk/_testing/helpers.py rename to src/globus_sdk/testing/helpers.py diff --git a/src/globus_sdk/_testing/models.py b/src/globus_sdk/testing/models.py similarity index 100% rename from src/globus_sdk/_testing/models.py rename to src/globus_sdk/testing/models.py diff --git a/src/globus_sdk/_testing/registry.py b/src/globus_sdk/testing/registry.py similarity index 96% rename from src/globus_sdk/_testing/registry.py rename to src/globus_sdk/testing/registry.py index 0f9c3cee9..b575f1e0e 100644 --- a/src/globus_sdk/_testing/registry.py +++ b/src/globus_sdk/testing/registry.py @@ -85,17 +85,17 @@ def get_response_set(set_id: t.Any) -> ResponseSet: # if ID is a string, it's the (optionally dotted) name of a module if isinstance(set_id, str): - module_name = f"globus_sdk._testing.data.{set_id}" + module_name = f"globus_sdk.testing.data.{set_id}" else: assert hasattr( set_id, "__qualname__" ), f"cannot load response set from {type(set_id)}" # support modules like - # globus_sdk/_testing/data/auth/get_identities.py + # globus_sdk/testing/data/auth/get_identities.py # for lookups like # get_response_set(AuthClient.get_identities) module_name = ( - f"globus_sdk._testing.data.{_resolve_qualname(set_id.__qualname__)}" + f"globus_sdk.testing.data.{_resolve_qualname(set_id.__qualname__)}" ) # after that, check the built-in "registry" built from modules diff --git a/tests/functional/base_client/test_advanced_http_options.py b/tests/functional/base_client/test_advanced_http_options.py index b3d1e91d2..61c3640d9 100644 --- a/tests/functional/base_client/test_advanced_http_options.py +++ b/tests/functional/base_client/test_advanced_http_options.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import RegisteredResponse, load_response +from globus_sdk.testing import RegisteredResponse, load_response def test_allow_redirects_false(client): diff --git a/tests/functional/base_client/test_default_headers.py b/tests/functional/base_client/test_default_headers.py index 8629d7a3a..258f5389f 100644 --- a/tests/functional/base_client/test_default_headers.py +++ b/tests/functional/base_client/test_default_headers.py @@ -1,5 +1,5 @@ from globus_sdk import __version__ -from globus_sdk._testing import RegisteredResponse, get_last_request +from globus_sdk.testing import RegisteredResponse, get_last_request def test_clientinfo_header_default(client): diff --git a/tests/functional/base_client/test_filter_missing.py b/tests/functional/base_client/test_filter_missing.py index 127994ef9..f1b10c422 100644 --- a/tests/functional/base_client/test_filter_missing.py +++ b/tests/functional/base_client/test_filter_missing.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import MISSING -from globus_sdk._testing import RegisteredResponse, get_last_request, load_response +from globus_sdk.testing import RegisteredResponse, get_last_request, load_response @pytest.fixture(autouse=True) diff --git a/tests/functional/base_client/test_retry_behavior.py b/tests/functional/base_client/test_retry_behavior.py index 8c8e67ac9..edabc272d 100644 --- a/tests/functional/base_client/test_retry_behavior.py +++ b/tests/functional/base_client/test_retry_behavior.py @@ -2,7 +2,7 @@ import requests import globus_sdk -from globus_sdk._testing import RegisteredResponse, load_response +from globus_sdk.testing import RegisteredResponse, load_response @pytest.mark.parametrize("error_status", [500, 429, 502, 503, 504]) diff --git a/tests/functional/globus_app/test_globus_app_token_handling.py b/tests/functional/globus_app/test_globus_app_token_handling.py index ecf7e8810..4c2188643 100644 --- a/tests/functional/globus_app/test_globus_app_token_handling.py +++ b/tests/functional/globus_app/test_globus_app_token_handling.py @@ -5,7 +5,7 @@ import globus_sdk import globus_sdk.token_storage -from globus_sdk._testing import RegisteredResponse, load_response +from globus_sdk.testing import RegisteredResponse, load_response # the JWT will have a client ID in its audience claim # make sure to use that value when trying to decode it diff --git a/tests/functional/local_endpoint/test_personal.py b/tests/functional/local_endpoint/test_personal.py index 027e2a4c6..3f6dad88d 100644 --- a/tests/functional/local_endpoint/test_personal.py +++ b/tests/functional/local_endpoint/test_personal.py @@ -6,7 +6,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response _IS_WINDOWS = os.name == "nt" diff --git a/tests/functional/login_flows/test_login_flow_manager.py b/tests/functional/login_flows/test_login_flow_manager.py index 4b951c687..5b0773bf8 100644 --- a/tests/functional/login_flows/test_login_flow_manager.py +++ b/tests/functional/login_flows/test_login_flow_manager.py @@ -3,7 +3,6 @@ import pytest from globus_sdk import ConfidentialAppAuthClient, NativeAppAuthClient -from globus_sdk._testing import load_response from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows import ( CommandLineLoginFlowManager, @@ -12,6 +11,7 @@ from globus_sdk.login_flows.command_line_login_flow_manager import ( CommandLineLoginFlowEOFError, ) +from globus_sdk.testing import load_response def _mock_input(s): diff --git a/tests/functional/services/auth/base/test_oauth2_revoke_token.py b/tests/functional/services/auth/base/test_oauth2_revoke_token.py index 725fae17e..df31d6614 100644 --- a/tests/functional/services/auth/base/test_oauth2_revoke_token.py +++ b/tests/functional/services/auth/base/test_oauth2_revoke_token.py @@ -4,7 +4,7 @@ import pytest import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.fixture diff --git a/tests/functional/services/auth/base/test_oauth2_validate_token.py b/tests/functional/services/auth/base/test_oauth2_validate_token.py index 1e67a8d91..af766fbeb 100644 --- a/tests/functional/services/auth/base/test_oauth2_validate_token.py +++ b/tests/functional/services/auth/base/test_oauth2_validate_token.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import RegisteredResponse, load_response +from globus_sdk.testing import RegisteredResponse, load_response def test_oauth2_validate_token_emits_deprecation_warning(): diff --git a/tests/functional/services/auth/confidential_client/test_create_child_client.py b/tests/functional/services/auth/confidential_client/test_create_child_client.py index 340247213..e8371ccd7 100644 --- a/tests/functional/services/auth/confidential_client/test_create_child_client.py +++ b/tests/functional/services/auth/confidential_client/test_create_child_client.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py b/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py index 20faf6ebb..9aef09995 100644 --- a/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py +++ b/tests/functional/services/auth/confidential_client/test_oauth2_client_credentials_tokens.py @@ -1,7 +1,7 @@ import urllib.parse -from globus_sdk._testing import get_last_request, load_response from globus_sdk.scopes import Scope +from globus_sdk.testing import get_last_request, load_response def test_oauth2_client_credentials_tokens(auth_client): diff --git a/tests/functional/services/auth/confidential_client/test_oauth2_get_dependent_tokens.py b/tests/functional/services/auth/confidential_client/test_oauth2_get_dependent_tokens.py index 67efc5c21..c6570152e 100644 --- a/tests/functional/services/auth/confidential_client/test_oauth2_get_dependent_tokens.py +++ b/tests/functional/services/auth/confidential_client/test_oauth2_get_dependent_tokens.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_oauth2_get_dependent_tokens(auth_client): diff --git a/tests/functional/services/auth/confidential_client/test_oauth2_token_introspect.py b/tests/functional/services/auth/confidential_client/test_oauth2_token_introspect.py index f31a1939f..8ed54f812 100644 --- a/tests/functional/services/auth/confidential_client/test_oauth2_token_introspect.py +++ b/tests/functional/services/auth/confidential_client/test_oauth2_token_introspect.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_oauth2_token_introspect(auth_client): diff --git a/tests/functional/services/auth/native_client/test_create_native_app_instance.py b/tests/functional/services/auth/native_client/test_create_native_app_instance.py index dd52a6d9d..2b9b07cba 100644 --- a/tests/functional/services/auth/native_client/test_create_native_app_instance.py +++ b/tests/functional/services/auth/native_client/test_create_native_app_instance.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_create_client.py b/tests/functional/services/auth/service_client/test_create_client.py index 7643c99d5..1fa5b6432 100644 --- a/tests/functional/services/auth/service_client/test_create_client.py +++ b/tests/functional/services/auth/service_client/test_create_client.py @@ -5,7 +5,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_create_client_credential.py b/tests/functional/services/auth/service_client/test_create_client_credential.py index 6096acb58..a718b65e7 100644 --- a/tests/functional/services/auth/service_client/test_create_client_credential.py +++ b/tests/functional/services/auth/service_client/test_create_client_credential.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize("uuid_type", (str, uuid.UUID)) diff --git a/tests/functional/services/auth/service_client/test_create_policy.py b/tests/functional/services/auth/service_client/test_create_policy.py index 5c204ea22..7d61f4a93 100644 --- a/tests/functional/services/auth/service_client/test_create_policy.py +++ b/tests/functional/services/auth/service_client/test_create_policy.py @@ -5,7 +5,7 @@ import pytest from globus_sdk import exc -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_create_project.py b/tests/functional/services/auth/service_client/test_create_project.py index 0caba212a..c649945b3 100644 --- a/tests/functional/services/auth/service_client/test_create_project.py +++ b/tests/functional/services/auth/service_client/test_create_project.py @@ -3,7 +3,7 @@ import pytest -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_create_scope.py b/tests/functional/services/auth/service_client/test_create_scope.py index c7ea900f0..ae0b6e5a8 100644 --- a/tests/functional/services/auth/service_client/test_create_scope.py +++ b/tests/functional/services/auth/service_client/test_create_scope.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_delete_client.py b/tests/functional/services/auth/service_client/test_delete_client.py index af4c20fa3..b03aff940 100644 --- a/tests/functional/services/auth/service_client/test_delete_client.py +++ b/tests/functional/services/auth/service_client/test_delete_client.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_delete_client_credential.py b/tests/functional/services/auth/service_client/test_delete_client_credential.py index 01cf8ff1a..22dd6da43 100644 --- a/tests/functional/services/auth/service_client/test_delete_client_credential.py +++ b/tests/functional/services/auth/service_client/test_delete_client_credential.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_delete_policy.py b/tests/functional/services/auth/service_client/test_delete_policy.py index 538554c42..39c0a3d7f 100644 --- a/tests/functional/services/auth/service_client/test_delete_policy.py +++ b/tests/functional/services/auth/service_client/test_delete_policy.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_delete_project.py b/tests/functional/services/auth/service_client/test_delete_project.py index b4c55b42a..f0946cb2b 100644 --- a/tests/functional/services/auth/service_client/test_delete_project.py +++ b/tests/functional/services/auth/service_client/test_delete_project.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_delete_project(service_client): diff --git a/tests/functional/services/auth/service_client/test_delete_scope.py b/tests/functional/services/auth/service_client/test_delete_scope.py index bd672f272..48e398b34 100644 --- a/tests/functional/services/auth/service_client/test_delete_scope.py +++ b/tests/functional/services/auth/service_client/test_delete_scope.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_get_client.py b/tests/functional/services/auth/service_client/test_get_client.py index 478f55ef8..5ed5717fb 100644 --- a/tests/functional/services/auth/service_client/test_get_client.py +++ b/tests/functional/services/auth/service_client/test_get_client.py @@ -5,7 +5,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_get_client_credentials.py b/tests/functional/services/auth/service_client/test_get_client_credentials.py index 9cbc97992..868669805 100644 --- a/tests/functional/services/auth/service_client/test_get_client_credentials.py +++ b/tests/functional/services/auth/service_client/test_get_client_credentials.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_get_clients.py b/tests/functional/services/auth/service_client/test_get_clients.py index 6565fe22d..5d670ce90 100644 --- a/tests/functional/services/auth/service_client/test_get_clients.py +++ b/tests/functional/services/auth/service_client/test_get_clients.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_clients(service_client): diff --git a/tests/functional/services/auth/service_client/test_get_consents.py b/tests/functional/services/auth/service_client/test_get_consents.py index 792b47d0b..c7cf2dea6 100644 --- a/tests/functional/services/auth/service_client/test_get_consents.py +++ b/tests/functional/services/auth/service_client/test_get_consents.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_consents(service_client): diff --git a/tests/functional/services/auth/service_client/test_get_identities.py b/tests/functional/services/auth/service_client/test_get_identities.py index 2efc29bfb..453643ad2 100644 --- a/tests/functional/services/auth/service_client/test_get_identities.py +++ b/tests/functional/services/auth/service_client/test_get_identities.py @@ -3,7 +3,7 @@ import pytest import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response class StringWrapper: diff --git a/tests/functional/services/auth/service_client/test_get_identity_providers.py b/tests/functional/services/auth/service_client/test_get_identity_providers.py index e7e3b7e49..3c7d041a3 100644 --- a/tests/functional/services/auth/service_client/test_get_identity_providers.py +++ b/tests/functional/services/auth/service_client/test_get_identity_providers.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_get_identity_providers_by_domains(service_client): diff --git a/tests/functional/services/auth/service_client/test_get_policies.py b/tests/functional/services/auth/service_client/test_get_policies.py index 69f8ae4d5..38579ccd4 100644 --- a/tests/functional/services/auth/service_client/test_get_policies.py +++ b/tests/functional/services/auth/service_client/test_get_policies.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_policies(service_client): diff --git a/tests/functional/services/auth/service_client/test_get_policy.py b/tests/functional/services/auth/service_client/test_get_policy.py index 1a06e5004..9f4df5da9 100644 --- a/tests/functional/services/auth/service_client/test_get_policy.py +++ b/tests/functional/services/auth/service_client/test_get_policy.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_get_project.py b/tests/functional/services/auth/service_client/test_get_project.py index f809a3d41..d817fadf6 100644 --- a/tests/functional/services/auth/service_client/test_get_project.py +++ b/tests/functional/services/auth/service_client/test_get_project.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_get_projects.py b/tests/functional/services/auth/service_client/test_get_projects.py index 35f15df47..e6336c42a 100644 --- a/tests/functional/services/auth/service_client/test_get_projects.py +++ b/tests/functional/services/auth/service_client/test_get_projects.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_projects(service_client): diff --git a/tests/functional/services/auth/service_client/test_get_scope.py b/tests/functional/services/auth/service_client/test_get_scope.py index bf13cac7d..249bd62e0 100644 --- a/tests/functional/services/auth/service_client/test_get_scope.py +++ b/tests/functional/services/auth/service_client/test_get_scope.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_get_scopes.py b/tests/functional/services/auth/service_client/test_get_scopes.py index 760cea606..2f4354257 100644 --- a/tests/functional/services/auth/service_client/test_get_scopes.py +++ b/tests/functional/services/auth/service_client/test_get_scopes.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_scopes(service_client): diff --git a/tests/functional/services/auth/service_client/test_update_client.py b/tests/functional/services/auth/service_client/test_update_client.py index 687a42582..efd25c64c 100644 --- a/tests/functional/services/auth/service_client/test_update_client.py +++ b/tests/functional/services/auth/service_client/test_update_client.py @@ -5,7 +5,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_update_policy.py b/tests/functional/services/auth/service_client/test_update_policy.py index b68350e18..ca981807f 100644 --- a/tests/functional/services/auth/service_client/test_update_policy.py +++ b/tests/functional/services/auth/service_client/test_update_policy.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_update_project.py b/tests/functional/services/auth/service_client/test_update_project.py index c755dbd32..185b7a6ff 100644 --- a/tests/functional/services/auth/service_client/test_update_project.py +++ b/tests/functional/services/auth/service_client/test_update_project.py @@ -4,7 +4,7 @@ import pytest from globus_sdk._missing import MISSING, filter_missing -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_update_scope.py b/tests/functional/services/auth/service_client/test_update_scope.py index 956325222..e3bed2cdc 100644 --- a/tests/functional/services/auth/service_client/test_update_scope.py +++ b/tests/functional/services/auth/service_client/test_update_scope.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/auth/service_client/test_userinfo.py b/tests/functional/services/auth/service_client/test_userinfo.py index 3ecd1dee7..5d8ff6263 100644 --- a/tests/functional/services/auth/service_client/test_userinfo.py +++ b/tests/functional/services/auth/service_client/test_userinfo.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response # TODO: add data for the success case and test it diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index c31606a2b..c0549dfdd 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -5,9 +5,9 @@ import globus_sdk from globus_sdk._missing import MISSING -from globus_sdk._testing import load_response from globus_sdk.scopes import TransferScopes from globus_sdk.services.auth.flow_managers.native_app import _make_native_app_challenge +from globus_sdk.testing import load_response CLIENT_ID = "d0f1d9b0-bd81-4108-be74-ea981664453a" diff --git a/tests/functional/services/auth/test_identity_map.py b/tests/functional/services/auth/test_identity_map.py index 8c944273c..46b18eef3 100644 --- a/tests/functional/services/auth/test_identity_map.py +++ b/tests/functional/services/auth/test_identity_map.py @@ -2,7 +2,7 @@ import responses import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response IDENTITIES_MULTIPLE_RESPONSE = { "identities": [ diff --git a/tests/functional/services/compute/v2/test_delete_endpoint.py b/tests/functional/services/compute/v2/test_delete_endpoint.py index 0ab619080..a3043c45c 100644 --- a/tests/functional/services/compute/v2/test_delete_endpoint.py +++ b/tests/functional/services/compute/v2/test_delete_endpoint.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_delete_endpoint(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_delete_function.py b/tests/functional/services/compute/v2/test_delete_function.py index eab3f08b4..d10d9377a 100644 --- a/tests/functional/services/compute/v2/test_delete_function.py +++ b/tests/functional/services/compute/v2/test_delete_function.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_delete_function(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_endpoint.py b/tests/functional/services/compute/v2/test_get_endpoint.py index 58dd98019..0a8366e5c 100644 --- a/tests/functional/services/compute/v2/test_get_endpoint.py +++ b/tests/functional/services/compute/v2/test_get_endpoint.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_endpoint(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_endpoint_status.py b/tests/functional/services/compute/v2/test_get_endpoint_status.py index 487530e63..b8a00b992 100644 --- a/tests/functional/services/compute/v2/test_get_endpoint_status.py +++ b/tests/functional/services/compute/v2/test_get_endpoint_status.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_endpoint_status(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_endpoints.py b/tests/functional/services/compute/v2/test_get_endpoints.py index 4c8e07eb5..268fa29db 100644 --- a/tests/functional/services/compute/v2/test_get_endpoints.py +++ b/tests/functional/services/compute/v2/test_get_endpoints.py @@ -1,7 +1,7 @@ import urllib.parse import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_get_endpoints(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_function.py b/tests/functional/services/compute/v2/test_get_function.py index 1da30dbea..c5867b218 100644 --- a/tests/functional/services/compute/v2/test_get_function.py +++ b/tests/functional/services/compute/v2/test_get_function.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_function(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_result_amqp_url.py b/tests/functional/services/compute/v2/test_get_result_amqp_url.py index 6f148bac6..6c1edd2dc 100644 --- a/tests/functional/services/compute/v2/test_get_result_amqp_url.py +++ b/tests/functional/services/compute/v2/test_get_result_amqp_url.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_result_amqp_url(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_task_batch.py b/tests/functional/services/compute/v2/test_get_task_batch.py index 4bdcaafad..190bd2e0f 100644 --- a/tests/functional/services/compute/v2/test_get_task_batch.py +++ b/tests/functional/services/compute/v2/test_get_task_batch.py @@ -4,7 +4,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/compute/v2/test_get_task_group.py b/tests/functional/services/compute/v2/test_get_task_group.py index dc2368d39..34a2c7660 100644 --- a/tests/functional/services/compute/v2/test_get_task_group.py +++ b/tests/functional/services/compute/v2/test_get_task_group.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_task_group(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_task_info.py b/tests/functional/services/compute/v2/test_get_task_info.py index aeadccf10..2394a9af2 100644 --- a/tests/functional/services/compute/v2/test_get_task_info.py +++ b/tests/functional/services/compute/v2/test_get_task_info.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_task(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_get_version.py b/tests/functional/services/compute/v2/test_get_version.py index 6b2b8594e..3b287b6dd 100644 --- a/tests/functional/services/compute/v2/test_get_version.py +++ b/tests/functional/services/compute/v2/test_get_version.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_version(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_lock_endpoint.py b/tests/functional/services/compute/v2/test_lock_endpoint.py index 1848212a6..2f3736c02 100644 --- a/tests/functional/services/compute/v2/test_lock_endpoint.py +++ b/tests/functional/services/compute/v2/test_lock_endpoint.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_lock_endpoint(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_register_endpoint.py b/tests/functional/services/compute/v2/test_register_endpoint.py index 6d9d094f0..66310dabe 100644 --- a/tests/functional/services/compute/v2/test_register_endpoint.py +++ b/tests/functional/services/compute/v2/test_register_endpoint.py @@ -1,7 +1,7 @@ import uuid import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response ENDPOINT_CONFIG = """ display_name: My Endpoint diff --git a/tests/functional/services/compute/v2/test_register_function.py b/tests/functional/services/compute/v2/test_register_function.py index 20b0e966d..7a93bd598 100644 --- a/tests/functional/services/compute/v2/test_register_function.py +++ b/tests/functional/services/compute/v2/test_register_function.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_register_function(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v2/test_submit.py b/tests/functional/services/compute/v2/test_submit.py index 90bc307f4..a208f258e 100644 --- a/tests/functional/services/compute/v2/test_submit.py +++ b/tests/functional/services/compute/v2/test_submit.py @@ -1,7 +1,7 @@ import uuid import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_submit(compute_client_v2: globus_sdk.ComputeClientV2): diff --git a/tests/functional/services/compute/v3/test_get_endpoint_allowlist.py b/tests/functional/services/compute/v3/test_get_endpoint_allowlist.py index 5c5ac653a..72ab162ec 100644 --- a/tests/functional/services/compute/v3/test_get_endpoint_allowlist.py +++ b/tests/functional/services/compute/v3/test_get_endpoint_allowlist.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_endpoint_allowlist(compute_client_v3: globus_sdk.ComputeClientV3): diff --git a/tests/functional/services/compute/v3/test_lock_endpoint.py b/tests/functional/services/compute/v3/test_lock_endpoint.py index d2d7558c4..6b91718c8 100644 --- a/tests/functional/services/compute/v3/test_lock_endpoint.py +++ b/tests/functional/services/compute/v3/test_lock_endpoint.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_lock_endpoint(compute_client_v3: globus_sdk.ComputeClientV3): diff --git a/tests/functional/services/compute/v3/test_register_endpoint.py b/tests/functional/services/compute/v3/test_register_endpoint.py index 94abfe5bb..945a9be30 100644 --- a/tests/functional/services/compute/v3/test_register_endpoint.py +++ b/tests/functional/services/compute/v3/test_register_endpoint.py @@ -1,7 +1,7 @@ import uuid import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response ENDPOINT_CONFIG = """ display_name: My Endpoint diff --git a/tests/functional/services/compute/v3/test_register_function.py b/tests/functional/services/compute/v3/test_register_function.py index c16ff0d74..77990b457 100644 --- a/tests/functional/services/compute/v3/test_register_function.py +++ b/tests/functional/services/compute/v3/test_register_function.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_register_function(compute_client_v3: globus_sdk.ComputeClientV3): diff --git a/tests/functional/services/compute/v3/test_submit.py b/tests/functional/services/compute/v3/test_submit.py index 2560938c6..2e5f02f6a 100644 --- a/tests/functional/services/compute/v3/test_submit.py +++ b/tests/functional/services/compute/v3/test_submit.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_submit(compute_client_v3: globus_sdk.ComputeClientV3): diff --git a/tests/functional/services/compute/v3/test_update_endpoint.py b/tests/functional/services/compute/v3/test_update_endpoint.py index 987680e7c..e7830e902 100644 --- a/tests/functional/services/compute/v3/test_update_endpoint.py +++ b/tests/functional/services/compute/v3/test_update_endpoint.py @@ -1,7 +1,7 @@ import uuid import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response ENDPOINT_CONFIG = """ display_name: My Endpoint diff --git a/tests/functional/services/flows/test_flow_crud.py b/tests/functional/services/flows/test_flow_crud.py index 750b8cf82..df1ef8502 100644 --- a/tests/functional/services/flows/test_flow_crud.py +++ b/tests/functional/services/flows/test_flow_crud.py @@ -4,8 +4,8 @@ from responses import matchers from globus_sdk import MISSING, FlowsAPIError -from globus_sdk._testing import get_last_request, load_response -from globus_sdk._testing.models import RegisteredResponse +from globus_sdk.testing import get_last_request, load_response +from globus_sdk.testing.models import RegisteredResponse @pytest.mark.parametrize("subscription_id", [MISSING, None, "dummy_subscription_id"]) diff --git a/tests/functional/services/flows/test_flow_validate.py b/tests/functional/services/flows/test_flow_validate.py index 668de99e2..0497343a2 100644 --- a/tests/functional/services/flows/test_flow_validate.py +++ b/tests/functional/services/flows/test_flow_validate.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import MISSING, FlowsAPIError -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize("input_schema", [MISSING, {}]) diff --git a/tests/functional/services/flows/test_get_run.py b/tests/functional/services/flows/test_get_run.py index 7944c572f..cf50b3a42 100644 --- a/tests/functional/services/flows/test_get_run.py +++ b/tests/functional/services/flows/test_get_run.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import MISSING -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize("include_flow_description", (MISSING, False, True)) diff --git a/tests/functional/services/flows/test_get_run_logs.py b/tests/functional/services/flows/test_get_run_logs.py index 70961659a..5ff00eb6e 100644 --- a/tests/functional/services/flows/test_get_run_logs.py +++ b/tests/functional/services/flows/test_get_run_logs.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_run_logs(flows_client): diff --git a/tests/functional/services/flows/test_list_flows.py b/tests/functional/services/flows/test_list_flows.py index 6cffff532..4d7239b81 100644 --- a/tests/functional/services/flows/test_list_flows.py +++ b/tests/functional/services/flows/test_list_flows.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import MISSING, GlobusSDKUsageError, RemovedInV4Warning -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize("filter_fulltext", [MISSING, "foo"]) diff --git a/tests/functional/services/flows/test_list_runs.py b/tests/functional/services/flows/test_list_runs.py index 9edc08a32..350adae1b 100644 --- a/tests/functional/services/flows/test_list_runs.py +++ b/tests/functional/services/flows/test_list_runs.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import MISSING -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_list_runs_simple(flows_client): diff --git a/tests/functional/services/flows/test_resume_run.py b/tests/functional/services/flows/test_resume_run.py index 6ff23307b..bd1bfbc83 100644 --- a/tests/functional/services/flows/test_resume_run.py +++ b/tests/functional/services/flows/test_resume_run.py @@ -1,7 +1,7 @@ import typing as t from globus_sdk import SpecificFlowClient -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_resume_run(specific_flow_client_class: t.Type[SpecificFlowClient]): diff --git a/tests/functional/services/flows/test_run_crud.py b/tests/functional/services/flows/test_run_crud.py index d8596971b..e9c30de53 100644 --- a/tests/functional/services/flows/test_run_crud.py +++ b/tests/functional/services/flows/test_run_crud.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import FlowsAPIError -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_get_run_definition(flows_client): diff --git a/tests/functional/services/flows/test_run_flow.py b/tests/functional/services/flows/test_run_flow.py index c836b5911..116ba3876 100644 --- a/tests/functional/services/flows/test_run_flow.py +++ b/tests/functional/services/flows/test_run_flow.py @@ -6,7 +6,7 @@ import globus_sdk from globus_sdk import FlowsAPIError, SpecificFlowClient -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_run_flow(specific_flow_client_class: type[SpecificFlowClient]): diff --git a/tests/functional/services/flows/test_validate_run.py b/tests/functional/services/flows/test_validate_run.py index 20e3ba1d8..980392c71 100644 --- a/tests/functional/services/flows/test_validate_run.py +++ b/tests/functional/services/flows/test_validate_run.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import FlowsAPIError, SpecificFlowClient -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_validate_run(specific_flow_client_class: type[SpecificFlowClient]): diff --git a/tests/functional/services/gcs/test_endpoints.py b/tests/functional/services/gcs/test_endpoints.py index 34499ed9c..2c4de1f51 100644 --- a/tests/functional/services/gcs/test_endpoints.py +++ b/tests/functional/services/gcs/test_endpoints.py @@ -1,5 +1,5 @@ import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_endpoint(client): diff --git a/tests/functional/services/gcs/test_get_collection_list.py b/tests/functional/services/gcs/test_get_collection_list.py index 95ec90a3f..eae2c7ee9 100644 --- a/tests/functional/services/gcs/test_get_collection_list.py +++ b/tests/functional/services/gcs/test_get_collection_list.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import MISSING, GCSAPIError -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_get_collection_list(client): diff --git a/tests/functional/services/gcs/test_get_gcs_info.py b/tests/functional/services/gcs/test_get_gcs_info.py index a20d1256e..8324dd8db 100644 --- a/tests/functional/services/gcs/test_get_gcs_info.py +++ b/tests/functional/services/gcs/test_get_gcs_info.py @@ -1,5 +1,5 @@ -from globus_sdk._testing import get_last_request, load_response from globus_sdk.authorizers import AccessTokenAuthorizer +from globus_sdk.testing import get_last_request, load_response def test_get_gcs_info(client): diff --git a/tests/functional/services/gcs/test_roles.py b/tests/functional/services/gcs/test_roles.py index 2fae98d2c..2bc545f7e 100644 --- a/tests/functional/services/gcs/test_roles.py +++ b/tests/functional/services/gcs/test_roles.py @@ -1,7 +1,7 @@ import json from globus_sdk import GCSRoleDocument -from globus_sdk._testing import get_last_request +from globus_sdk.testing import get_last_request from tests.common import register_api_route_fixture_file diff --git a/tests/functional/services/gcs/test_storage_gateways.py b/tests/functional/services/gcs/test_storage_gateways.py index 747d08cbe..f4c935ea0 100644 --- a/tests/functional/services/gcs/test_storage_gateways.py +++ b/tests/functional/services/gcs/test_storage_gateways.py @@ -4,7 +4,7 @@ import globus_sdk from globus_sdk import MISSING -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/gcs/test_user_credential.py b/tests/functional/services/gcs/test_user_credential.py index b302552ce..61c092d7d 100644 --- a/tests/functional/services/gcs/test_user_credential.py +++ b/tests/functional/services/gcs/test_user_credential.py @@ -1,7 +1,7 @@ import json from globus_sdk import ConnectorTable, UserCredentialDocument -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_get_user_credential_list(client): diff --git a/tests/functional/services/groups/test_create_group.py b/tests/functional/services/groups/test_create_group.py index 209a82361..b630a948c 100644 --- a/tests/functional/services/groups/test_create_group.py +++ b/tests/functional/services/groups/test_create_group.py @@ -1,6 +1,6 @@ import json -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_create_group(groups_client): diff --git a/tests/functional/services/groups/test_delete_group.py b/tests/functional/services/groups/test_delete_group.py index a3ff9a679..cf2b86361 100644 --- a/tests/functional/services/groups/test_delete_group.py +++ b/tests/functional/services/groups/test_delete_group.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_delete_group(groups_client): diff --git a/tests/functional/services/groups/test_get_group.py b/tests/functional/services/groups/test_get_group.py index 58ea84575..ed3902f04 100644 --- a/tests/functional/services/groups/test_get_group.py +++ b/tests/functional/services/groups/test_get_group.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_get_group(groups_client): diff --git a/tests/functional/services/groups/test_get_group_by_subscription_id.py b/tests/functional/services/groups/test_get_group_by_subscription_id.py index 455597503..432c4790f 100644 --- a/tests/functional/services/groups/test_get_group_by_subscription_id.py +++ b/tests/functional/services/groups/test_get_group_by_subscription_id.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_get_group_by_subscription_id(groups_client): diff --git a/tests/functional/services/groups/test_get_my_groups.py b/tests/functional/services/groups/test_get_my_groups.py index 6eaa2e375..e794b3f17 100644 --- a/tests/functional/services/groups/test_get_my_groups.py +++ b/tests/functional/services/groups/test_get_my_groups.py @@ -1,5 +1,5 @@ -from globus_sdk._testing import load_response from globus_sdk.response import ArrayResponse +from globus_sdk.testing import load_response def test_get_my_groups(groups_client): diff --git a/tests/functional/services/groups/test_group_memberships.py b/tests/functional/services/groups/test_group_memberships.py index eb8275ffb..769eeaeac 100644 --- a/tests/functional/services/groups/test_group_memberships.py +++ b/tests/functional/services/groups/test_group_memberships.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import BatchMembershipActions, GroupRole -from globus_sdk._testing import RegisteredResponse, get_last_request, load_response +from globus_sdk.testing import RegisteredResponse, get_last_request, load_response from tests.common import register_api_route_fixture_file diff --git a/tests/functional/services/groups/test_set_group_policies.py b/tests/functional/services/groups/test_set_group_policies.py index 7c25b822c..e181ee1d4 100644 --- a/tests/functional/services/groups/test_set_group_policies.py +++ b/tests/functional/services/groups/test_set_group_policies.py @@ -9,7 +9,7 @@ GroupRequiredSignupFields, GroupVisibility, ) -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/search/test_batch_delete_by_subject.py b/tests/functional/services/search/test_batch_delete_by_subject.py index 1814380f1..c3cf2a2c7 100644 --- a/tests/functional/services/search/test_batch_delete_by_subject.py +++ b/tests/functional/services/search/test_batch_delete_by_subject.py @@ -1,6 +1,6 @@ import json -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_batch_delete_by_subject(client): diff --git a/tests/functional/services/search/test_create_index.py b/tests/functional/services/search/test_create_index.py index e2310d6d7..93ad691d5 100644 --- a/tests/functional/services/search/test_create_index.py +++ b/tests/functional/services/search/test_create_index.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_create_index(client): diff --git a/tests/functional/services/search/test_delete_index.py b/tests/functional/services/search/test_delete_index.py index 835c62b63..45d8f0fc3 100644 --- a/tests/functional/services/search/test_delete_index.py +++ b/tests/functional/services/search/test_delete_index.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_delete_index(client): diff --git a/tests/functional/services/search/test_index_list.py b/tests/functional/services/search/test_index_list.py index b66020272..be2d2b494 100644 --- a/tests/functional/services/search/test_index_list.py +++ b/tests/functional/services/search/test_index_list.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_search_index_list(client): diff --git a/tests/functional/services/search/test_reopen_index.py b/tests/functional/services/search/test_reopen_index.py index b5ef9a05a..8f6a034b9 100644 --- a/tests/functional/services/search/test_reopen_index.py +++ b/tests/functional/services/search/test_reopen_index.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_reopen_index(client): diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index 218f714a4..4e5f6f5d3 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -7,7 +7,7 @@ import globus_sdk from globus_sdk._missing import filter_missing -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response from tests.common import register_api_route_fixture_file diff --git a/tests/functional/services/search/test_search_roles.py b/tests/functional/services/search/test_search_roles.py index 55b151036..5efa4a80b 100644 --- a/tests/functional/services/search/test_search_roles.py +++ b/tests/functional/services/search/test_search_roles.py @@ -3,7 +3,7 @@ import pytest import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.fixture diff --git a/tests/functional/services/timers/test_create_timer.py b/tests/functional/services/timers/test_create_timer.py index a62909aaa..4167f704f 100644 --- a/tests/functional/services/timers/test_create_timer.py +++ b/tests/functional/services/timers/test_create_timer.py @@ -2,7 +2,7 @@ import globus_sdk from globus_sdk._missing import filter_missing -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_dummy_timer_creation(client): diff --git a/tests/functional/services/timers/test_jobs.py b/tests/functional/services/timers/test_jobs.py index 54160d7e5..973385a0a 100644 --- a/tests/functional/services/timers/test_jobs.py +++ b/tests/functional/services/timers/test_jobs.py @@ -4,8 +4,8 @@ import pytest from globus_sdk import TimerJob, TimersAPIError, TransferData, config, exc -from globus_sdk._testing import get_last_request, load_response from globus_sdk._utils import slash_join +from globus_sdk.testing import get_last_request, load_response from tests.common import GO_EP1_ID, GO_EP2_ID diff --git a/tests/functional/services/transfer/endpoint_manager/test_endpoint_manager_task_successful_transfers.py b/tests/functional/services/transfer/endpoint_manager/test_endpoint_manager_task_successful_transfers.py index 528a2977c..113a7727e 100644 --- a/tests/functional/services/transfer/endpoint_manager/test_endpoint_manager_task_successful_transfers.py +++ b/tests/functional/services/transfer/endpoint_manager/test_endpoint_manager_task_successful_transfers.py @@ -1,4 +1,4 @@ -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_endpoint_manager_task_successful_transfers(client): diff --git a/tests/functional/services/transfer/endpoint_manager/test_task_event_list.py b/tests/functional/services/transfer/endpoint_manager/test_task_event_list.py index d6042cd6e..a4158f4db 100644 --- a/tests/functional/services/transfer/endpoint_manager/test_task_event_list.py +++ b/tests/functional/services/transfer/endpoint_manager/test_task_event_list.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import get_last_request +from globus_sdk.testing import get_last_request from tests.common import register_api_route ZERO_ID = uuid.UUID(int=0) diff --git a/tests/functional/services/transfer/endpoint_manager/test_task_list.py b/tests/functional/services/transfer/endpoint_manager/test_task_list.py index f86a46ddb..fb458ae7d 100644 --- a/tests/functional/services/transfer/endpoint_manager/test_task_list.py +++ b/tests/functional/services/transfer/endpoint_manager/test_task_list.py @@ -4,7 +4,7 @@ import pytest import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response ZERO_ID = uuid.UUID(int=0) diff --git a/tests/functional/services/transfer/test_operation_ls.py b/tests/functional/services/transfer/test_operation_ls.py index 68f3107e3..d9ec532a9 100644 --- a/tests/functional/services/transfer/test_operation_ls.py +++ b/tests/functional/services/transfer/test_operation_ls.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import RegisteredResponse, get_last_request, load_response +from globus_sdk.testing import RegisteredResponse, get_last_request, load_response from tests.common import GO_EP1_ID diff --git a/tests/functional/services/transfer/test_operation_mkdir.py b/tests/functional/services/transfer/test_operation_mkdir.py index 844dadeca..e9111acb8 100644 --- a/tests/functional/services/transfer/test_operation_mkdir.py +++ b/tests/functional/services/transfer/test_operation_mkdir.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import MISSING -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response _OMIT = object() diff --git a/tests/functional/services/transfer/test_operation_rename.py b/tests/functional/services/transfer/test_operation_rename.py index 981593320..e4e59e0d4 100644 --- a/tests/functional/services/transfer/test_operation_rename.py +++ b/tests/functional/services/transfer/test_operation_rename.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import MISSING -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response _OMIT = object() diff --git a/tests/functional/services/transfer/test_operation_stat.py b/tests/functional/services/transfer/test_operation_stat.py index 479a7af1a..3a0588a3d 100644 --- a/tests/functional/services/transfer/test_operation_stat.py +++ b/tests/functional/services/transfer/test_operation_stat.py @@ -4,7 +4,7 @@ import urllib.parse -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response def test_operation_stat(client): diff --git a/tests/functional/services/transfer/test_operation_symlink.py b/tests/functional/services/transfer/test_operation_symlink.py index de85f72d1..6cda727fa 100644 --- a/tests/functional/services/transfer/test_operation_symlink.py +++ b/tests/functional/services/transfer/test_operation_symlink.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import exc -from globus_sdk._testing import RegisteredResponse +from globus_sdk.testing import RegisteredResponse @pytest.fixture diff --git a/tests/functional/services/transfer/test_set_subscription_admin_verified.py b/tests/functional/services/transfer/test_set_subscription_admin_verified.py index 033f86071..5e741e67f 100644 --- a/tests/functional/services/transfer/test_set_subscription_admin_verified.py +++ b/tests/functional/services/transfer/test_set_subscription_admin_verified.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_set_subscription_admin_verified(client): diff --git a/tests/functional/services/transfer/test_set_subscription_id.py b/tests/functional/services/transfer/test_set_subscription_id.py index 2676baa82..201431a49 100644 --- a/tests/functional/services/transfer/test_set_subscription_id.py +++ b/tests/functional/services/transfer/test_set_subscription_id.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_set_subscription_id(client): diff --git a/tests/functional/services/transfer/test_simple.py b/tests/functional/services/transfer/test_simple.py index a280e3799..ce670db3a 100644 --- a/tests/functional/services/transfer/test_simple.py +++ b/tests/functional/services/transfer/test_simple.py @@ -5,7 +5,7 @@ import pytest import globus_sdk -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response from tests.common import GO_EP1_ID, register_api_route_fixture_file diff --git a/tests/functional/services/transfer/test_task_list.py b/tests/functional/services/transfer/test_task_list.py index c066c60a1..09d9fdaa4 100644 --- a/tests/functional/services/transfer/test_task_list.py +++ b/tests/functional/services/transfer/test_task_list.py @@ -2,7 +2,7 @@ import pytest -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize( diff --git a/tests/functional/services/transfer/test_task_submit.py b/tests/functional/services/transfer/test_task_submit.py index 822ad0b5b..a6a7c788d 100644 --- a/tests/functional/services/transfer/test_task_submit.py +++ b/tests/functional/services/transfer/test_task_submit.py @@ -7,7 +7,7 @@ import pytest from globus_sdk import DeleteData, TransferAPIError, TransferData -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response from tests.common import GO_EP1_ID, GO_EP2_ID diff --git a/tests/functional/services/transfer/test_task_wait.py b/tests/functional/services/transfer/test_task_wait.py index 61099a4c4..05b9d0045 100644 --- a/tests/functional/services/transfer/test_task_wait.py +++ b/tests/functional/services/transfer/test_task_wait.py @@ -4,7 +4,7 @@ import responses import globus_sdk -from globus_sdk._testing import get_last_request +from globus_sdk.testing import get_last_request from tests.common import register_api_route_fixture_file TASK1_ID = "b8872740-7edc-11ec-9f33-ed182a728dff" diff --git a/tests/functional/_testing/test_non_default_mock.py b/tests/functional/testing/test_non_default_mock.py similarity index 91% rename from tests/functional/_testing/test_non_default_mock.py rename to tests/functional/testing/test_non_default_mock.py index 7267e2c48..52a527e5a 100644 --- a/tests/functional/_testing/test_non_default_mock.py +++ b/tests/functional/testing/test_non_default_mock.py @@ -1,5 +1,5 @@ """ -Test that globus_sdk._testing can accept a non-default requests mock +Test that globus_sdk.testing can accept a non-default requests mock """ import sys @@ -9,7 +9,7 @@ import responses from globus_sdk import GlobusHTTPResponse, GroupsClient -from globus_sdk._testing import get_last_request, load_response +from globus_sdk.testing import get_last_request, load_response @pytest.fixture diff --git a/tests/functional/tokenstorage/v2/conftest.py b/tests/functional/tokenstorage/v2/conftest.py index f00823c35..ed7acb4e9 100644 --- a/tests/functional/tokenstorage/v2/conftest.py +++ b/tests/functional/tokenstorage/v2/conftest.py @@ -5,7 +5,7 @@ import pytest import globus_sdk -from globus_sdk._testing import RegisteredResponse +from globus_sdk.testing import RegisteredResponse from globus_sdk.token_storage import TokenStorageData diff --git a/tests/unit/errors/test_auth_errors.py b/tests/unit/errors/test_auth_errors.py index 0103a2feb..7a8ae060f 100644 --- a/tests/unit/errors/test_auth_errors.py +++ b/tests/unit/errors/test_auth_errors.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import AuthAPIError -from globus_sdk._testing import construct_error +from globus_sdk.testing import construct_error def test_auth_error_get_args_simple(): diff --git a/tests/unit/errors/test_common_functionality.py b/tests/unit/errors/test_common_functionality.py index 95997229e..dff25e365 100644 --- a/tests/unit/errors/test_common_functionality.py +++ b/tests/unit/errors/test_common_functionality.py @@ -4,7 +4,7 @@ import requests from globus_sdk import ErrorSubdocument, GlobusAPIError, RemovedInV4Warning, exc -from globus_sdk._testing import construct_error +from globus_sdk.testing import construct_error def _strmatch_any_order(inputstr, prefix, midfixes, suffix, sep=", "): diff --git a/tests/unit/errors/test_timers_errors.py b/tests/unit/errors/test_timers_errors.py index 5125d495a..99b0e1ca8 100644 --- a/tests/unit/errors/test_timers_errors.py +++ b/tests/unit/errors/test_timers_errors.py @@ -1,5 +1,5 @@ from globus_sdk import TimersAPIError -from globus_sdk._testing import construct_error +from globus_sdk.testing import construct_error def test_timer_error_load_simple(): diff --git a/tests/unit/errors/test_transfer_errors.py b/tests/unit/errors/test_transfer_errors.py index b88291d2a..5e621acfa 100644 --- a/tests/unit/errors/test_transfer_errors.py +++ b/tests/unit/errors/test_transfer_errors.py @@ -1,5 +1,5 @@ from globus_sdk import TransferAPIError -from globus_sdk._testing import construct_error +from globus_sdk.testing import construct_error def test_transfer_response_get_args(): diff --git a/tests/unit/globus_app/test_client_integration.py b/tests/unit/globus_app/test_client_integration.py index e676eb5ec..236713c4d 100644 --- a/tests/unit/globus_app/test_client_integration.py +++ b/tests/unit/globus_app/test_client_integration.py @@ -4,7 +4,7 @@ import globus_sdk from globus_sdk import GlobusApp, GlobusAppConfig, UserApp -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response from globus_sdk.token_storage import MemoryTokenStorage diff --git a/tests/unit/globus_app/test_globus_app.py b/tests/unit/globus_app/test_globus_app.py index 7149871ac..db0672feb 100644 --- a/tests/unit/globus_app/test_globus_app.py +++ b/tests/unit/globus_app/test_globus_app.py @@ -19,7 +19,6 @@ TransferClient, UserApp, ) -from globus_sdk._testing import load_response from globus_sdk.exc import GlobusSDKUsageError from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.globus_app.authorizer_factory import ( @@ -33,6 +32,7 @@ LoginFlowManager, ) from globus_sdk.scopes import AuthScopes, Scope +from globus_sdk.testing import load_response from globus_sdk.token_storage import ( HasRefreshTokensValidator, JSONTokenStorage, diff --git a/tests/unit/sphinxext/test_expand_testing_fixture.py b/tests/unit/sphinxext/test_expand_testing_fixture.py index 15311c9aa..46de311f9 100644 --- a/tests/unit/sphinxext/test_expand_testing_fixture.py +++ b/tests/unit/sphinxext/test_expand_testing_fixture.py @@ -20,7 +20,7 @@ def test_expand_testing_fixture_fails_on_bad_reference(sphinx_runner, capsys): pytest.fail("Didn't find 'ValueError: no fixtures defined' in stderr") assert ( - "no fixtures defined for globus_sdk._testing.data.NO_SUCH_FIXTURE" in test_line + "no fixtures defined for globus_sdk.testing.data.NO_SUCH_FIXTURE" in test_line ) diff --git a/tests/unit/test_auth_requirements_error.py b/tests/unit/test_auth_requirements_error.py index 0ec203a92..345f57c9f 100644 --- a/tests/unit/test_auth_requirements_error.py +++ b/tests/unit/test_auth_requirements_error.py @@ -2,7 +2,6 @@ import pytest -from globus_sdk._testing import construct_error from globus_sdk.exc import ErrorSubdocument from globus_sdk.gare import ( GARE, @@ -13,6 +12,7 @@ to_gare, to_gares, ) +from globus_sdk.testing import construct_error @pytest.mark.parametrize( diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index 0b08f689c..df1abcd5e 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -7,9 +7,9 @@ import globus_sdk from globus_sdk import GlobusApp, GlobusAppConfig, GlobusSDKUsageError, UserApp -from globus_sdk._testing import RegisteredResponse, get_last_request from globus_sdk.authorizers import NullAuthorizer from globus_sdk.scopes import Scope, TransferScopes +from globus_sdk.testing import RegisteredResponse, get_last_request from globus_sdk.token_storage import TokenValidationError diff --git a/tests/unit/test_gcs_client.py b/tests/unit/test_gcs_client.py index e48976592..356c9f73c 100644 --- a/tests/unit/test_gcs_client.py +++ b/tests/unit/test_gcs_client.py @@ -1,5 +1,5 @@ from globus_sdk import GCSClient -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_client_address_handling(): diff --git a/tests/unit/_testing/test_construct_error.py b/tests/unit/testing/test_construct_error.py similarity index 97% rename from tests/unit/_testing/test_construct_error.py rename to tests/unit/testing/test_construct_error.py index 9a976936e..203392a3a 100644 --- a/tests/unit/_testing/test_construct_error.py +++ b/tests/unit/testing/test_construct_error.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import construct_error +from globus_sdk.testing import construct_error def test_construct_error_defaults_to_base_error_class(): diff --git a/tests/unit/_testing/test_registered_response.py b/tests/unit/testing/test_registered_response.py similarity index 91% rename from tests/unit/_testing/test_registered_response.py rename to tests/unit/testing/test_registered_response.py index 11837e6d9..a0af10cb3 100644 --- a/tests/unit/_testing/test_registered_response.py +++ b/tests/unit/testing/test_registered_response.py @@ -4,7 +4,7 @@ import pytest -from globus_sdk._testing import RegisteredResponse +from globus_sdk.testing import RegisteredResponse @pytest.mark.skipif( From 7accb3c77be4efb0dced8897288f9ededcc79365 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 10 Jul 2025 13:41:24 -0500 Subject: [PATCH 086/176] Remove deprecated 'consents' experimental module This is now in `globus_sdk.scopes.consents`; no tests or current source uses the deprecated name. --- docs/upgrading.rst | 1 + src/globus_sdk/experimental/consents.py | 39 ------------------------- 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 src/globus_sdk/experimental/consents.py diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7be36710f..1a3ba6586 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -239,6 +239,7 @@ The removed alias and new module names are shown in the table below. "``globus_sdk.experimental.auth_requirements_error``", "``globus_sdk.gare``" "``globus_sdk.experimental.scope_parser``", "``globus_sdk.scopes``" + "``globus_sdk.experimental.consents``", "``globus_sdk.scopes.consents``" "``globus_sdk.experimental.tokenstorage``", "``globus_sdk.token_storage``" ``MutableScope`` is Removed, use ``Scope`` Instead diff --git a/src/globus_sdk/experimental/consents.py b/src/globus_sdk/experimental/consents.py deleted file mode 100644 index 85cc295db..000000000 --- a/src/globus_sdk/experimental/consents.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -import sys -import typing as t - -__all__ = ( - "Consent", - "ConsentTree", - "ConsentForest", - "ConsentParseError", - "ConsentTreeConstructionError", -) - -# legacy aliases -# (when accessed, these will emit deprecation warnings in a future release) -if t.TYPE_CHECKING: - from globus_sdk.scopes.consents import ( - Consent, - ConsentForest, - ConsentParseError, - ConsentTree, - ConsentTreeConstructionError, - ) -else: - - def __getattr__(name: str) -> t.Any: - import globus_sdk.scopes.consents as consents_module - from globus_sdk.exc import warn_deprecated - - warn_deprecated( - "'globus_sdk.experimental.consents' has been renamed to " - "'globus_sdk.scopes.consents'. " - f"Importing '{name}' from `globus_sdk.experimental` is deprecated." - ) - value = getattr(consents_module, name, None) - if value is None: - raise AttributeError(f"module {__name__} has no attribute {name}") - setattr(sys.modules[__name__], name, value) - return value From cfbca5ee4bb64ea17e1bfdb031952ca98aee6c9f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 11 Jul 2025 09:37:14 -0500 Subject: [PATCH 087/176] Fix PR links in changelog (#1256) --- changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.rst b/changelog.rst index 2c38c002b..e824e3f99 100644 --- a/changelog.rst +++ b/changelog.rst @@ -85,7 +85,7 @@ Changed (:pr:`1222`) - The SDK's ``ScopeBuilder`` types have been replaced with - ``StaticScopeCollection`` and ``DynamicScopeCollection`` types. (:pr:`NUMBER`) + ``StaticScopeCollection`` and ``DynamicScopeCollection`` types. (:pr:`1237`) - Scopes provided as constants by the SDK are now ``Scope`` objects, not strings. They can be converted to strings trivially with ``str(scope)``. @@ -96,7 +96,7 @@ Changed ``GCSCollectionScopeBuilder``. - The ``ScopeBuilder`` types have been simplified and improved as the new - ``ScopeCollection`` types. (:pr:`NUMBER`) + ``ScopeCollection`` types. (:pr:`1237`) - ``ScopeBuilder`` is replaced with ``StaticScopeCollection`` and ``DynamicScopeCollection``. The ``scopes`` attribute of client classes is From cf1a8672326dd29102b494813dcb61e42065d564 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 14 Jul 2025 13:42:49 -0500 Subject: [PATCH 088/176] Remove the compatibility shim from `create_policy` (#1257) * Remove the compatibility shim from `create_policy` * Remove unused lint ignore + fix param order Documentation order now matches declaration order. --- .../services/auth/client/service_client.py | 48 ++--------------- .../auth/service_client/test_create_policy.py | 51 +------------------ 2 files changed, 5 insertions(+), 94 deletions(-) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index a7cacb748..67b9e3c01 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -1,6 +1,5 @@ from __future__ import annotations -import functools import logging import typing as t @@ -36,44 +35,6 @@ F = t.TypeVar("F", bound=t.Callable[..., GlobusHTTPResponse]) -def _create_policy_compat(f: F) -> F: - @functools.wraps(f) - def wrapper(self: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any: - if args: - if len(args) > 5: - raise TypeError( - "create_policy() takes 5 positional arguments " - f"but {len(args)} were given" - ) - - exc.warn_deprecated( - "'AuthClient.create_policy' received positional arguments. " - "Use only keyword arguments instead." - ) - - for argname, argvalue in zip( - ( - "project_id", - "high_assurance", - "authentication_assurance_timeout", - "required_mfa", - "display_name", - "description", - ), - args, - ): - if argname in kwargs: - raise TypeError( - f"create_policy() got multiple values for argument '{argname}'" - ) - else: - kwargs[argname] = argvalue - - return f(self, **kwargs) - - return t.cast(F, wrapper) - - class AuthClient(client.BaseClient): """ A client for using the @@ -803,8 +764,7 @@ def get_policies(self) -> IterableResponse: """ return GetPoliciesResponse(self.get("/v2/api/policies")) - @_create_policy_compat - def create_policy( # pylint: disable=missing-param-doc + def create_policy( self, *, project_id: UUIDLike, @@ -820,15 +780,15 @@ def create_policy( # pylint: disable=missing-param-doc Create a new Auth policy. Requires the ``manage_projects`` scope. :param project_id: ID of the project for the new policy + :param display_name: A user-friendly name for the policy + :param description: A user-friendly description to explain the purpose of the + policy :param high_assurance: Whether or not this policy is applied to sessions. :param authentication_assurance_timeout: Number of seconds within which someone must have authenticated to satisfy the policy :param required_mfa: If True, then multi-factor authentication is required. This can only be set to True for high-assurance policies. The default is False. - :param display_name: A user-friendly name for the policy - :param description: A user-friendly description to explain the purpose of the - policy :param domain_constraints_include: A list of domains that can satisfy the policy :param domain_constraints_exclude: A list of domains that cannot satisfy the policy diff --git a/tests/functional/services/auth/service_client/test_create_policy.py b/tests/functional/services/auth/service_client/test_create_policy.py index 7d61f4a93..125d5cc2a 100644 --- a/tests/functional/services/auth/service_client/test_create_policy.py +++ b/tests/functional/services/auth/service_client/test_create_policy.py @@ -1,11 +1,8 @@ from __future__ import annotations -import json - import pytest -from globus_sdk import exc -from globus_sdk.testing import get_last_request, load_response +from globus_sdk.testing import load_response @pytest.mark.parametrize( @@ -36,49 +33,3 @@ def test_create_policy( res = service_client.create_policy(**meta["args"]) for k, v in meta["response"].items(): assert res["policy"][k] == v - - -def test_compatible_create_policy_usage_rejects_too_many_positionals(service_client): - load_response(service_client.create_policy) - with pytest.raises( - TypeError, - match=r"create_policy\(\) takes 5 positional arguments but 6 were given", - ): - service_client.create_policy(1, 2, 3, 4, 5, 6) - - -def test_valid_compatible_policy_usage_emits_warning(service_client): - load_response(service_client.create_policy) - with pytest.warns( - exc.RemovedInV4Warning, - match=r"'AuthClient\.create_policy' received positional arguments", - ): - service_client.create_policy( - "my_project_id", - True, - 101, - display_name="my_display_name", - description="my_description", - ) - - lastreq = get_last_request() - sent_data = json.loads(lastreq.body) - assert sent_data["policy"] == { - "project_id": "my_project_id", - "high_assurance": True, - "authentication_assurance_timeout": 101, - "display_name": "my_display_name", - "description": "my_description", - } - - -def test_policy_usage_warns_and_errors_when_argument_is_supplied_twice(service_client): - with pytest.raises( - TypeError, - match="create_policy\\(\\) got multiple values for argument 'project_id'", - ): - with pytest.warns( - exc.RemovedInV4Warning, - match="'AuthClient.create_policy()' received positional arguments", - ): - service_client.create_policy("my_project_id", project_id="my_project_id2") From 55bc971cb144f1b5b511e952acb2a63267a0b4f9 Mon Sep 17 00:00:00 2001 From: Max Tuecke Date: Wed, 16 Jul 2025 10:47:52 -0500 Subject: [PATCH 089/176] Replace `ScopeCollectionType` alias with explicit expansion of types (#1259) --- ...sc_41884_replace_scope_collection_type.rst | 4 ++ src/globus_sdk/_types.py | 10 ---- .../authorizers/client_credentials.py | 5 +- src/globus_sdk/client.py | 10 ++-- src/globus_sdk/globus_app/app.py | 13 ++++-- src/globus_sdk/globus_app/client_app.py | 9 +++- src/globus_sdk/globus_app/user_app.py | 6 ++- src/globus_sdk/scopes/_normalize.py | 46 +++++++++++-------- src/globus_sdk/services/auth/_common.py | 7 +-- .../auth/client/confidential_client.py | 7 +-- .../services/auth/client/native_client.py | 5 +- .../auth/flow_managers/authorization_code.py | 4 +- .../services/auth/flow_managers/native_app.py | 4 +- .../app_scope_requirements.py | 13 +++--- .../scope_collection_type.py | 5 +- tests/unit/scopes/test_scope_normalization.py | 32 +++++++++++-- 16 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst diff --git a/changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst b/changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst new file mode 100644 index 000000000..175cc6b1c --- /dev/null +++ b/changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst @@ -0,0 +1,4 @@ +Changed +------- + +- Remove support for normalizing nested iterables of scopes, e.g. ``[["scope1"], "scope2"]`` (:pr:`1259`) diff --git a/src/globus_sdk/_types.py b/src/globus_sdk/_types.py index 331e591c5..c2a1d1997 100644 --- a/src/globus_sdk/_types.py +++ b/src/globus_sdk/_types.py @@ -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 diff --git a/src/globus_sdk/authorizers/client_credentials.py b/src/globus_sdk/authorizers/client_credentials.py index 9dd59e1e2..c9614b74c 100644 --- a/src/globus_sdk/authorizers/client_credentials.py +++ b/src/globus_sdk/authorizers/client_credentials.py @@ -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 @@ -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, diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 6b5cecedb..f1bfb2022 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -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 @@ -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 @@ -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:: diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index 374c7c901..72e5f6d9b 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -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 @@ -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 @@ -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 {} @@ -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 diff --git a/src/globus_sdk/globus_app/client_app.py b/src/globus_sdk/globus_app/client_app.py index de145d467..8c8f9fe0e 100644 --- a/src/globus_sdk/globus_app/client_app.py +++ b/src/globus_sdk/globus_app/client_app.py @@ -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 @@ -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: diff --git a/src/globus_sdk/globus_app/user_app.py b/src/globus_sdk/globus_app/user_app.py index 6346971f3..5de12f977 100644 --- a/src/globus_sdk/globus_app/user_app.py +++ b/src/globus_sdk/globus_app/user_app.py @@ -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 ( @@ -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__( diff --git a/src/globus_sdk/scopes/_normalize.py b/src/globus_sdk/scopes/_normalize.py index e88112913..2c9af3f57 100644 --- a/src/globus_sdk/scopes/_normalize.py +++ b/src/globus_sdk/scopes/_normalize.py @@ -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: @@ -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: @@ -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 split. This flag allows a caller to optimize, skipping a bfs operation if merging will be done later purely with strings. @@ -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]: @@ -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)}") diff --git a/src/globus_sdk/services/auth/_common.py b/src/globus_sdk/services/auth/_common.py index 9cb5f2c96..55b5a33c7 100644 --- a/src/globus_sdk/services/auth/_common.py +++ b/src/globus_sdk/services/auth/_common.py @@ -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( diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 7e966824c..3419c440a 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -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 @@ -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 @@ -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, diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index 5c8667c7f..996c761a8 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -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 @@ -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", diff --git a/src/globus_sdk/services/auth/flow_managers/authorization_code.py b/src/globus_sdk/services/auth/flow_managers/authorization_code.py index 3373425fa..af01a5e9e 100644 --- a/src/globus_sdk/services/auth/flow_managers/authorization_code.py +++ b/src/globus_sdk/services/auth/flow_managers/authorization_code.py @@ -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 @@ -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: diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index 3267acf43..df88b037d 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -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 @@ -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, diff --git a/tests/non-pytest/mypy-ignore-tests/app_scope_requirements.py b/tests/non-pytest/mypy-ignore-tests/app_scope_requirements.py index ed643abce..b49b214c5 100644 --- a/tests/non-pytest/mypy-ignore-tests/app_scope_requirements.py +++ b/tests/non-pytest/mypy-ignore-tests/app_scope_requirements.py @@ -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) diff --git a/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py b/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py index a82235dab..ff25aeb73 100644 --- a/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py +++ b/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py @@ -1,5 +1,6 @@ +import typing as t + import globus_sdk -from globus_sdk._types import ScopeCollectionType from globus_sdk.scopes import Scope, scopes_to_str from globus_sdk.services.auth import ( GlobusAuthorizationCodeFlowManager, @@ -22,7 +23,7 @@ # this function should type-check okay -def foo(x: ScopeCollectionType) -> str: +def foo(x: str | Scope | t.Iterable[str | Scope]) -> str: return scopes_to_str(x) diff --git a/tests/unit/scopes/test_scope_normalization.py b/tests/unit/scopes/test_scope_normalization.py index afde75fbb..1b12598a1 100644 --- a/tests/unit/scopes/test_scope_normalization.py +++ b/tests/unit/scopes/test_scope_normalization.py @@ -31,16 +31,28 @@ def test_scopes_to_str_roundtrip_simple_str_in_collection(scope_collection): ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), ( - ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), + (Scope("scope1"), Scope("scope2"), "scope3 scope4"), "scope1 scope2 scope3 scope4", ), - (([[["bar"]]],), "bar"), + ([Scope("scope1"), "scope2", "scope3 scope4"], "scope1 scope2 scope3 scope4"), ), ) def test_scopes_to_str_handles_mixed_data(scope_collection, expect_str): assert scopes_to_str(scope_collection) == expect_str +@pytest.mark.parametrize( + "scope_collection", + ( + ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), + [["bar"]], + ), +) +def test_scopes_to_str_rejects_nested_iterables(scope_collection): + with pytest.raises(TypeError): + scopes_to_str(scope_collection) + + @pytest.mark.parametrize( "scope_collection", ([Scope("scope1")], Scope("scope1"), "scope1"), @@ -61,10 +73,10 @@ def test_scopes_to_scope_list_simple(scope_collection): ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), ( - ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), + (Scope("scope1"), Scope("scope2"), "scope3 scope4"), "scope1 scope2 scope3 scope4", ), - (([[["bar"]]],), "bar"), + ([Scope("scope1"), "scope2", "scope3 scope4"], "scope1 scope2 scope3 scope4"), ), ) def test_scopes_to_scope_list_handles_mixed_data(scope_collection, expect_str): @@ -74,6 +86,18 @@ def test_scopes_to_scope_list_handles_mixed_data(scope_collection, expect_str): assert _as_sorted_string(actual_list) == expect_str +@pytest.mark.parametrize( + "scope_collection", + ( + ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), + [["bar"]], + ), +) +def test_scopes_to_list_rejects_nested_iterables(scope_collection): + with pytest.raises(TypeError): + scopes_to_scope_list(scope_collection) + + def test_scopes_to_scope_list_handles_dependent_scopes(): scope_collection = "scope1 scope2[scope3 scope4]" actual_list = scopes_to_scope_list(scope_collection) From dffca9cc8d0783cdc66ba06bf262d825bc28ef33 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 17:23:15 -0500 Subject: [PATCH 090/176] Create 'globus_sdk._internal', move 'lazy_import' Create a new subpackage for internal components. As the first piece, move the lazy importer. --- src/globus_sdk/__init__.py | 2 +- src/globus_sdk/_internal/__init__.py | 0 src/globus_sdk/{_lazy_import.py => _internal/lazy_import.py} | 0 tests/non-pytest/lazy-imports/test_for_import_cycles.py | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/globus_sdk/_internal/__init__.py rename src/globus_sdk/{_lazy_import.py => _internal/lazy_import.py} (100%) diff --git a/src/globus_sdk/__init__.py b/src/globus_sdk/__init__.py index c408ca509..4eaf17880 100644 --- a/src/globus_sdk/__init__.py +++ b/src/globus_sdk/__init__.py @@ -2,7 +2,7 @@ import logging import sys -from ._lazy_import import ( +from ._internal.lazy_import import ( default_dir_implementation, default_getattr_implementation, load_all_tuple, diff --git a/src/globus_sdk/_internal/__init__.py b/src/globus_sdk/_internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/globus_sdk/_lazy_import.py b/src/globus_sdk/_internal/lazy_import.py similarity index 100% rename from src/globus_sdk/_lazy_import.py rename to src/globus_sdk/_internal/lazy_import.py diff --git a/tests/non-pytest/lazy-imports/test_for_import_cycles.py b/tests/non-pytest/lazy-imports/test_for_import_cycles.py index f34ff634f..38cdb07a8 100644 --- a/tests/non-pytest/lazy-imports/test_for_import_cycles.py +++ b/tests/non-pytest/lazy-imports/test_for_import_cycles.py @@ -16,7 +16,7 @@ import pytest import globus_sdk -from globus_sdk._lazy_import import find_source_module +from globus_sdk._internal.lazy_import import find_source_module PYTHON_BINARY = os.environ.get("GLOBUS_TEST_PY", sys.executable) From 3538fa16daebc66986ec7af95dad90e2cee63f45 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 17:30:08 -0500 Subject: [PATCH 091/176] Move classproperty into _internal --- src/globus_sdk/{_classproperty.py => _internal/classprop.py} | 2 +- src/globus_sdk/client.py | 2 +- src/globus_sdk/services/gcs/client.py | 2 +- .../lazy-imports/test_modules_do_not_require_requests.py | 2 +- tests/unit/test_classproperty.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/globus_sdk/{_classproperty.py => _internal/classprop.py} (97%) diff --git a/src/globus_sdk/_classproperty.py b/src/globus_sdk/_internal/classprop.py similarity index 97% rename from src/globus_sdk/_classproperty.py rename to src/globus_sdk/_internal/classprop.py index d23162b14..80ea76865 100644 --- a/src/globus_sdk/_classproperty.py +++ b/src/globus_sdk/_internal/classprop.py @@ -5,7 +5,7 @@ Usage: - from globus_sdk._classproperty import classproperty + from globus_sdk._internal.classprop import classproperty class A: @classproperty diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index f1bfb2022..83312568c 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -6,7 +6,7 @@ import urllib.parse from globus_sdk import GlobusSDKUsageError, config, exc -from globus_sdk._classproperty import classproperty +from globus_sdk._internal.classprop import classproperty from globus_sdk._utils import slash_join from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.paging import PaginatorTable diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index baf5b6184..3ec399ee7 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -4,7 +4,7 @@ import uuid from globus_sdk import client, exc, paging, response -from globus_sdk._classproperty import classproperty +from globus_sdk._internal.classprop import classproperty from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index 8bd4faabe..d6535942a 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -35,7 +35,7 @@ # internal components and utilities are a special case: # failing to ensure that these avoid 'requests' can make it more difficult # to ensure that the main parts (above) do not transitively pick it up - "_classproperty", + "_internal.classprop", "_guards", "_missing", "_remarshal", diff --git a/tests/unit/test_classproperty.py b/tests/unit/test_classproperty.py index f399677a8..945ea4483 100644 --- a/tests/unit/test_classproperty.py +++ b/tests/unit/test_classproperty.py @@ -1,4 +1,4 @@ -from globus_sdk._classproperty import classproperty +from globus_sdk._internal.classprop import classproperty def test_classproperty_simple(): From c6b5963ae9c2930ac7067fa5f7b41c4d24a0f49c Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 17:37:43 -0500 Subject: [PATCH 092/176] Move the flake8 extension into _internal --- .flake8 | 4 ++-- src/globus_sdk/_internal/extensions/__init__.py | 0 .../extensions/globus_sdk_flake8.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/globus_sdk/_internal/extensions/__init__.py rename src/globus_sdk/{_globus_sdk_flake8.py => _internal/extensions/globus_sdk_flake8.py} (100%) diff --git a/.flake8 b/.flake8 index 492a84648..2a56c1def 100644 --- a/.flake8 +++ b/.flake8 @@ -8,5 +8,5 @@ per-file-ignores = *.pyi:E302,E305 [flake8:local-plugins] extension = - SDK = _globus_sdk_flake8:Plugin -paths = ./src/globus_sdk/ + SDK = globus_sdk_flake8:Plugin +paths = ./src/globus_sdk/_internal/extensions/ diff --git a/src/globus_sdk/_internal/extensions/__init__.py b/src/globus_sdk/_internal/extensions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/globus_sdk/_globus_sdk_flake8.py b/src/globus_sdk/_internal/extensions/globus_sdk_flake8.py similarity index 100% rename from src/globus_sdk/_globus_sdk_flake8.py rename to src/globus_sdk/_internal/extensions/globus_sdk_flake8.py From cae215a8c61bc2250b72e5cb3b8cc954e7b551ac Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 17:39:39 -0500 Subject: [PATCH 093/176] Move the sphinx extension into _internal --- docs/conf.py | 2 +- .../extensions/sphinxext}/__init__.py | 0 .../extensions/sphinxext}/autodoc_hooks.py | 0 .../extensions/sphinxext}/directives/__init__.py | 0 .../extensions/sphinxext}/directives/add_content_directive.py | 0 .../extensions/sphinxext}/directives/automethodlist.py | 0 .../extensions/sphinxext}/directives/copy_params.py | 0 .../sphinxext}/directives/enumerate_testing_fixtures.py | 0 .../sphinxext}/directives/expand_testing_fixture.py | 0 .../extensions/sphinxext}/directives/externaldoclink.py | 0 .../extensions/sphinxext}/directives/list_known_scopes.py | 0 .../extensions/sphinxext}/directives/paginated_usage.py | 0 .../{_sphinxext => _internal/extensions/sphinxext}/roles.py | 0 .../{_sphinxext => _internal/extensions/sphinxext}/utils.py | 0 tests/unit/sphinxext/conftest.py | 4 ++-- 15 files changed, 3 insertions(+), 3 deletions(-) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/__init__.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/autodoc_hooks.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/__init__.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/add_content_directive.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/automethodlist.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/copy_params.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/enumerate_testing_fixtures.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/expand_testing_fixture.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/externaldoclink.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/list_known_scopes.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/directives/paginated_usage.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/roles.py (100%) rename src/globus_sdk/{_sphinxext => _internal/extensions/sphinxext}/utils.py (100%) diff --git a/docs/conf.py b/docs/conf.py index 1075b1aa6..42fec9371 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ "sphinx_design", "sphinx_issues", # our custom one - "globus_sdk._sphinxext", + "globus_sdk._internal.extensions.sphinxext", ] project = "globus-sdk" diff --git a/src/globus_sdk/_sphinxext/__init__.py b/src/globus_sdk/_internal/extensions/sphinxext/__init__.py similarity index 100% rename from src/globus_sdk/_sphinxext/__init__.py rename to src/globus_sdk/_internal/extensions/sphinxext/__init__.py diff --git a/src/globus_sdk/_sphinxext/autodoc_hooks.py b/src/globus_sdk/_internal/extensions/sphinxext/autodoc_hooks.py similarity index 100% rename from src/globus_sdk/_sphinxext/autodoc_hooks.py rename to src/globus_sdk/_internal/extensions/sphinxext/autodoc_hooks.py diff --git a/src/globus_sdk/_sphinxext/directives/__init__.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/__init__.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/__init__.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/__init__.py diff --git a/src/globus_sdk/_sphinxext/directives/add_content_directive.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/add_content_directive.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/add_content_directive.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/add_content_directive.py diff --git a/src/globus_sdk/_sphinxext/directives/automethodlist.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/automethodlist.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/automethodlist.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/automethodlist.py diff --git a/src/globus_sdk/_sphinxext/directives/copy_params.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/copy_params.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/copy_params.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/copy_params.py diff --git a/src/globus_sdk/_sphinxext/directives/enumerate_testing_fixtures.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/enumerate_testing_fixtures.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/enumerate_testing_fixtures.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/enumerate_testing_fixtures.py diff --git a/src/globus_sdk/_sphinxext/directives/expand_testing_fixture.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/expand_testing_fixture.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/expand_testing_fixture.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/expand_testing_fixture.py diff --git a/src/globus_sdk/_sphinxext/directives/externaldoclink.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/externaldoclink.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/externaldoclink.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/externaldoclink.py diff --git a/src/globus_sdk/_sphinxext/directives/list_known_scopes.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/list_known_scopes.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/list_known_scopes.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/list_known_scopes.py diff --git a/src/globus_sdk/_sphinxext/directives/paginated_usage.py b/src/globus_sdk/_internal/extensions/sphinxext/directives/paginated_usage.py similarity index 100% rename from src/globus_sdk/_sphinxext/directives/paginated_usage.py rename to src/globus_sdk/_internal/extensions/sphinxext/directives/paginated_usage.py diff --git a/src/globus_sdk/_sphinxext/roles.py b/src/globus_sdk/_internal/extensions/sphinxext/roles.py similarity index 100% rename from src/globus_sdk/_sphinxext/roles.py rename to src/globus_sdk/_internal/extensions/sphinxext/roles.py diff --git a/src/globus_sdk/_sphinxext/utils.py b/src/globus_sdk/_internal/extensions/sphinxext/utils.py similarity index 100% rename from src/globus_sdk/_sphinxext/utils.py rename to src/globus_sdk/_internal/extensions/sphinxext/utils.py diff --git a/tests/unit/sphinxext/conftest.py b/tests/unit/sphinxext/conftest.py index e257e0a17..5c984ba36 100644 --- a/tests/unit/sphinxext/conftest.py +++ b/tests/unit/sphinxext/conftest.py @@ -183,9 +183,9 @@ def sphinxext(): """ pytest.importorskip("docutils", reason="testing sphinx extension needs docutils") - import globus_sdk._sphinxext + import globus_sdk._internal.extensions.sphinxext - return globus_sdk._sphinxext + return globus_sdk._internal.extensions.sphinxext @pytest.fixture From 677bfdf2969806e0d6ca201ddcfab367448953a1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 17:52:21 -0500 Subject: [PATCH 094/176] Move guards into _internal --- .../{_guards.py => _internal/guards.py} | 0 src/globus_sdk/_serializable.py | 2 +- src/globus_sdk/exc/api.py | 8 +- src/globus_sdk/exc/err_info.py | 10 +-- .../gare/_auth_requirements_error.py | 2 +- src/globus_sdk/gare/_variants.py | 2 +- src/globus_sdk/response.py | 4 +- .../services/auth/client/base_login_client.py | 5 +- src/globus_sdk/services/flows/client.py | 6 +- src/globus_sdk/services/flows/errors.py | 4 +- src/globus_sdk/services/timers/client.py | 7 +- src/globus_sdk/services/timers/errors.py | 6 +- src/globus_sdk/services/transfer/client.py | 7 +- src/globus_sdk/token_storage/v2/token_data.py | 2 +- .../test_modules_do_not_require_requests.py | 2 +- .../mypy-ignore-tests/test_guards.py | 16 ++-- tests/unit/test_guards.py | 77 +++++++++---------- 17 files changed, 80 insertions(+), 80 deletions(-) rename src/globus_sdk/{_guards.py => _internal/guards.py} (100%) diff --git a/src/globus_sdk/_guards.py b/src/globus_sdk/_internal/guards.py similarity index 100% rename from src/globus_sdk/_guards.py rename to src/globus_sdk/_internal/guards.py diff --git a/src/globus_sdk/_serializable.py b/src/globus_sdk/_serializable.py index 1eee60b51..97ffdf1a9 100644 --- a/src/globus_sdk/_serializable.py +++ b/src/globus_sdk/_serializable.py @@ -18,7 +18,7 @@ class Serializable: Serializable classes: - know what fields they have, based on their initializer signatures - support `to_dict()` and `from_dict()` conversions - - typically use `globus_sdk._guards.validators` to check attribute types + - typically use `globus_sdk._internal.guards.validators` to check attribute types """ _EXCLUDE_VARS: t.ClassVar[tuple[str, ...]] = ("self", "extra") diff --git a/src/globus_sdk/exc/api.py b/src/globus_sdk/exc/api.py index 93c7c904c..248ce5292 100644 --- a/src/globus_sdk/exc/api.py +++ b/src/globus_sdk/exc/api.py @@ -4,7 +4,7 @@ import logging import typing as t -from globus_sdk import _guards +from globus_sdk._internal import guards from .base import GlobusError from .err_info import ErrorInfoContainer @@ -268,7 +268,7 @@ def _detect_error_format(self) -> _ErrorFormat: # well-formed if self._jsonapi_mimetype(): errors = self._dict_data.get("errors") - if not _guards.is_list_of(errors, dict): + if not guards.is_list_of(errors, dict): return _ErrorFormat.undefined elif len(errors) < 1: return _ErrorFormat.undefined @@ -316,7 +316,7 @@ def _parse_type_zero_error_format(self) -> bool: self.code = self._dict_data["code"] self.messages = [self._dict_data["message"]] self.request_id = self._dict_data.get("request_id") - if _guards.is_list_of(self._dict_data.get("errors"), dict): + if guards.is_list_of(self._dict_data.get("errors"), dict): raw_errors = self._dict_data["errors"] else: raw_errors = [self._dict_data] @@ -334,7 +334,7 @@ def _parse_undefined_error_format(self) -> bool: """ # attempt to pull out errors if possible and valid - if _guards.is_list_of(self._dict_data.get("errors"), dict): + if guards.is_list_of(self._dict_data.get("errors"), dict): raw_errors = self._dict_data["errors"] # if no 'errors' were found, or 'errors' is invalid, then # 'errors' should be set to contain the root document diff --git a/src/globus_sdk/exc/err_info.py b/src/globus_sdk/exc/err_info.py index 2846c2dea..cd3bb8f9b 100644 --- a/src/globus_sdk/exc/err_info.py +++ b/src/globus_sdk/exc/err_info.py @@ -3,7 +3,7 @@ import logging import typing as t -from globus_sdk import _guards +from globus_sdk._internal import guards log = logging.getLogger(__name__) @@ -100,7 +100,7 @@ def _parse_session_required_identities( self, data: dict[str, t.Any] ) -> list[str] | None: session_required_identities = data.get("session_required_identities") - if _guards.is_list_of(session_required_identities, str): + if guards.is_list_of(session_required_identities, str): return session_required_identities elif session_required_identities is not None: self._warn_type( @@ -114,7 +114,7 @@ def _parse_session_required_single_domain( self, data: dict[str, t.Any] ) -> list[str] | None: session_required_single_domain = data.get("session_required_single_domain") - if _guards.is_list_of(session_required_single_domain, str): + if guards.is_list_of(session_required_single_domain, str): return session_required_single_domain elif session_required_single_domain is not None: self._warn_type( @@ -130,7 +130,7 @@ def _parse_session_required_policies( session_required_policies = data.get("session_required_policies") if isinstance(session_required_policies, str): return session_required_policies.split(",") - elif _guards.is_list_of(session_required_policies, str): + elif guards.is_list_of(session_required_policies, str): return session_required_policies elif session_required_policies is not None: self._warn_type( @@ -172,7 +172,7 @@ def __init__(self, error_data: dict[str, t.Any]) -> None: self._has_data = has_code and bool(self.required_scopes) def _parse_required_scopes(self, data: dict[str, t.Any]) -> list[str]: - if _guards.is_list_of(data.get("required_scopes"), str): + if guards.is_list_of(data.get("required_scopes"), str): return t.cast("list[str]", data["required_scopes"]) elif isinstance(data.get("required_scope"), str): return [data["required_scope"]] diff --git a/src/globus_sdk/gare/_auth_requirements_error.py b/src/globus_sdk/gare/_auth_requirements_error.py index 9082aff60..281413152 100644 --- a/src/globus_sdk/gare/_auth_requirements_error.py +++ b/src/globus_sdk/gare/_auth_requirements_error.py @@ -2,7 +2,7 @@ import typing as t -from globus_sdk._guards import validators +from globus_sdk._internal.guards import validators from globus_sdk._serializable import Serializable diff --git a/src/globus_sdk/gare/_variants.py b/src/globus_sdk/gare/_variants.py index 8ea9053c4..72b1bcdf6 100644 --- a/src/globus_sdk/gare/_variants.py +++ b/src/globus_sdk/gare/_variants.py @@ -3,7 +3,7 @@ import typing as t from globus_sdk import exc -from globus_sdk._guards import validators +from globus_sdk._internal.guards import validators from globus_sdk._serializable import Serializable from ._auth_requirements_error import GARE, GlobusAuthorizationParameters diff --git a/src/globus_sdk/response.py b/src/globus_sdk/response.py index ea2359ff0..a5c282c44 100644 --- a/src/globus_sdk/response.py +++ b/src/globus_sdk/response.py @@ -5,7 +5,7 @@ import logging import typing as t -from globus_sdk import _guards +from globus_sdk._internal import guards log = logging.getLogger(__name__) @@ -145,7 +145,7 @@ def get(self, key: str, default: t.Any = None) -> t.Any: :param key: The string key to lookup in the response if it is a dict :param default: The default value to be used if the data is null or a list """ - if _guards.is_optional(self.data, list): + if guards.is_optional(self.data, list): return default # NB: `default` is provided as a positional because the native dict type # doesn't recognize a keyword argument `default` diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 0075c892e..b4ec1042b 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -5,7 +5,8 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey -from globus_sdk import _guards, client, exc +from globus_sdk import client, exc +from globus_sdk._internal import guards from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike @@ -284,7 +285,7 @@ def oauth2_validate_token( # if this client has no way of authenticating itself but # it does have a client_id, we'll send that in the request - no_authentication = _guards.is_optional(self.authorizer, NullAuthorizer) + no_authentication = guards.is_optional(self.authorizer, NullAuthorizer) if no_authentication and self.client_id: log.debug("Validating token with unauthenticated client") body.update({"client_id": self.client_id}) diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index fd4e3ad0c..292332fea 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -8,11 +8,11 @@ from globus_sdk import ( GlobusHTTPResponse, GlobusSDKUsageError, - _guards, client, exc, paging, ) +from globus_sdk._internal import guards from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike @@ -992,14 +992,14 @@ def add_app_transfer_data_access_scope( client.run_flow({"collection": COLLECTION_ID}) """ # noqa: E501 if isinstance(collection_ids, (str, uuid.UUID)): - _guards.validators.uuidlike("collection_ids", collection_ids) + guards.validators.uuidlike("collection_ids", collection_ids) # wrap the collection_ids input in a list for consistent iteration below collection_ids_ = [collection_ids] else: # copy to a list so that ephemeral iterables can be iterated multiple times collection_ids_ = list(collection_ids) for i, c in enumerate(collection_ids_): - _guards.validators.uuidlike(f"collection_ids[{i}]", c) + guards.validators.uuidlike(f"collection_ids[{i}]", c) transfer_scope = TransferScopes.all.with_optional(True) for coll_id in collection_ids_: diff --git a/src/globus_sdk/services/flows/errors.py b/src/globus_sdk/services/flows/errors.py index 270a5adf0..e38031881 100644 --- a/src/globus_sdk/services/flows/errors.py +++ b/src/globus_sdk/services/flows/errors.py @@ -1,6 +1,6 @@ from __future__ import annotations -from globus_sdk import _guards +from globus_sdk._internal import guards from globus_sdk.exc import ErrorSubdocument, GlobusAPIError @@ -28,7 +28,7 @@ def _parse_undefined_error_format(self) -> bool: self.code = self._extract_code_from_error_array(self.errors) details = self._dict_data["error"].get("detail") - if _guards.is_list_of(details, dict): + if guards.is_list_of(details, dict): self.messages = [ error_detail["msg"] for error_detail in details diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index d57bf3b1b..270f7d5d6 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -4,7 +4,8 @@ import typing as t import uuid -from globus_sdk import _guards, client, exc, response +from globus_sdk import client, exc, response +from globus_sdk._internal import guards from globus_sdk._types import UUIDLike from globus_sdk.scopes import ( GCSCollectionScopes, @@ -77,14 +78,14 @@ def add_app_transfer_data_access_scope( client.create_timer(daily_timer) """ # noqa: E501 if isinstance(collection_ids, (str, uuid.UUID)): - _guards.validators.uuidlike("collection_ids", collection_ids) + guards.validators.uuidlike("collection_ids", collection_ids) # wrap the collection_ids input in a list for consistent iteration below collection_ids_ = [collection_ids] else: # copy to a list so that ephemeral iterables can be iterated multiple times collection_ids_ = list(collection_ids) for i, c in enumerate(collection_ids_): - _guards.validators.uuidlike(f"collection_ids[{i}]", c) + guards.validators.uuidlike(f"collection_ids[{i}]", c) dependencies: list[Scope] = [] for coll_id in collection_ids_: diff --git a/src/globus_sdk/services/timers/errors.py b/src/globus_sdk/services/timers/errors.py index 5654febdb..c03d1f2ed 100644 --- a/src/globus_sdk/services/timers/errors.py +++ b/src/globus_sdk/services/timers/errors.py @@ -2,7 +2,7 @@ import typing as t -from globus_sdk import _guards +from globus_sdk._internal import guards from globus_sdk.exc import ErrorSubdocument, GlobusAPIError @@ -53,7 +53,7 @@ def _parse_undefined_error_format(self) -> bool: self.code = self._extract_code_from_error_array(self.errors) self.messages = self._extract_messages_from_error_array(self.errors) return True - elif _guards.is_list_of(self._dict_data.get("detail"), dict): + elif guards.is_list_of(self._dict_data.get("detail"), dict): # collect the errors array from details self.errors = [ ErrorSubdocument(d, message_fields=("msg",)) @@ -79,6 +79,6 @@ def _parse_detail_docs( if d.message is None: continue loc_list = d.get("loc") - if not _guards.is_list_of(loc_list, str): + if not guards.is_list_of(loc_list, str): continue yield (d.message, ".".join(loc_list)) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index a4c8629fb..9857735eb 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -5,7 +5,8 @@ import typing as t import uuid -from globus_sdk import _guards, client, exc, paging, response +from globus_sdk import client, exc, paging, response +from globus_sdk._internal import guards from globus_sdk._missing import MISSING, MissingType from globus_sdk._remarshal import commajoin from globus_sdk._types import DateLike, IntLike, UUIDLike @@ -189,14 +190,14 @@ def add_app_data_access_scope( res = client.submit_transfer({}) """ # noqa: E501 if isinstance(collection_ids, (str, uuid.UUID)): - _guards.validators.uuidlike("collection_ids", collection_ids) + guards.validators.uuidlike("collection_ids", collection_ids) # wrap the collection_ids input in a list for consistent iteration below collection_ids_ = [collection_ids] else: # copy to a list so that ephemeral iterables can be iterated multiple times collection_ids_ = list(collection_ids) for i, c in enumerate(collection_ids_): - _guards.validators.uuidlike(f"collection_ids[{i}]", c) + guards.validators.uuidlike(f"collection_ids[{i}]", c) scope = TransferScopes.all dependencies: list[Scope] = [] diff --git a/src/globus_sdk/token_storage/v2/token_data.py b/src/globus_sdk/token_storage/v2/token_data.py index 1ddf106da..0668deac5 100644 --- a/src/globus_sdk/token_storage/v2/token_data.py +++ b/src/globus_sdk/token_storage/v2/token_data.py @@ -2,7 +2,7 @@ import typing as t -from globus_sdk._guards import validators +from globus_sdk._internal.guards import validators from globus_sdk._serializable import Serializable diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index d6535942a..fd70f8229 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -36,7 +36,7 @@ # failing to ensure that these avoid 'requests' can make it more difficult # to ensure that the main parts (above) do not transitively pick it up "_internal.classprop", - "_guards", + "_internal.guards", "_missing", "_remarshal", "_serializable", diff --git a/tests/non-pytest/mypy-ignore-tests/test_guards.py b/tests/non-pytest/mypy-ignore-tests/test_guards.py index 0dc761533..bd7b65b27 100644 --- a/tests/non-pytest/mypy-ignore-tests/test_guards.py +++ b/tests/non-pytest/mypy-ignore-tests/test_guards.py @@ -1,7 +1,7 @@ -# test that the internal _guards module provides valid and well-formed type-guards +# test that the internal guards module provides valid and well-formed type-guards import typing as t -from globus_sdk import _guards +from globus_sdk._internal import guards def get_any() -> t.Any: @@ -12,20 +12,20 @@ def get_any() -> t.Any: t.assert_type(x, t.Any) # test is_list_of -if _guards.is_list_of(x, str): +if guards.is_list_of(x, str): t.assert_type(x, list[str]) -elif _guards.is_list_of(x, int): +elif guards.is_list_of(x, int): t.assert_type(x, list[int]) # test is_optional -if _guards.is_optional(x, float): +if guards.is_optional(x, float): t.assert_type(x, float | None) -elif _guards.is_optional(x, bytes): +elif guards.is_optional(x, bytes): t.assert_type(x, bytes | None) # test is_optional_list_of -if _guards.is_optional_list_of(x, type(None)): +if guards.is_optional_list_of(x, type(None)): t.assert_type(x, list[None] | None) -elif _guards.is_optional_list_of(x, dict): +elif guards.is_optional_list_of(x, dict): t.assert_type(x, list[dict[t.Any, t.Any]] | None) diff --git a/tests/unit/test_guards.py b/tests/unit/test_guards.py index 07e833d06..4efe85e63 100644 --- a/tests/unit/test_guards.py +++ b/tests/unit/test_guards.py @@ -2,7 +2,8 @@ import pytest -from globus_sdk import _guards, _serializable, exc +from globus_sdk import _serializable, exc +from globus_sdk._internal import guards @pytest.mark.parametrize( @@ -25,7 +26,7 @@ ], ) def test_list_of_guard(value, typ, ok): - assert _guards.is_list_of(value, typ) == ok + assert guards.is_list_of(value, typ) == ok @pytest.mark.parametrize( @@ -41,7 +42,7 @@ def test_list_of_guard(value, typ, ok): ], ) def test_opt_guard(value, typ, ok): - assert _guards.is_optional(value, typ) == ok + assert guards.is_optional(value, typ) == ok @pytest.mark.parametrize( @@ -62,12 +63,12 @@ def test_opt_guard(value, typ, ok): ], ) def test_opt_list_guard(value, typ, ok): - assert _guards.is_optional_list_of(value, typ) == ok + assert guards.is_optional_list_of(value, typ) == ok @pytest.mark.parametrize("value", (uuid.UUID(int=0), str(uuid.UUID(int=1)))) def test_uuidlike_ok(value): - assert _guards.validators.uuidlike("foo", value) == value + assert guards.validators.uuidlike("foo", value) == value @pytest.mark.parametrize("value", (str(uuid.UUID(int=0))[:-1], "")) @@ -75,7 +76,7 @@ def test_uuidlike_fails_value(value): with pytest.raises( exc.ValidationError, match="'foo' must be a valid UUID" ) as excinfo: - _guards.validators.uuidlike("foo", value) + guards.validators.uuidlike("foo", value) err = excinfo.value assert f"value='{value}'" in str(err) @@ -86,7 +87,7 @@ def test_uuidlike_fails_type(value): with pytest.raises( exc.ValidationError, match="'foo' must be a UUID or str" ) as excinfo: - _guards.validators.uuidlike("foo", value) + guards.validators.uuidlike("foo", value) err = excinfo.value assert f"value='{value}'" in str(err) @@ -95,33 +96,31 @@ def test_uuidlike_fails_type(value): @pytest.mark.parametrize( "validator, value", ( - pytest.param(_guards.validators.str_, "bar", id="str"), - pytest.param(_guards.validators.int_, 0, id="int-0"), - pytest.param(_guards.validators.int_, 1, id="int-1"), - pytest.param(_guards.validators.opt_str, "bar", id="opt_str-str"), - pytest.param(_guards.validators.opt_str, None, id="opt_str-None"), - pytest.param(_guards.validators.opt_bool, True, id="opt_bool-True"), - pytest.param(_guards.validators.opt_bool, False, id="opt_bool-False"), - pytest.param(_guards.validators.opt_bool, None, id="opt_bool-None"), - pytest.param(_guards.validators.str_list, [], id="str_list-empty"), - pytest.param(_guards.validators.str_list, ["foo"], id="str_list-onestr"), - pytest.param(_guards.validators.opt_str_list, [], id="opt_str_list-empty"), + pytest.param(guards.validators.str_, "bar", id="str"), + pytest.param(guards.validators.int_, 0, id="int-0"), + pytest.param(guards.validators.int_, 1, id="int-1"), + pytest.param(guards.validators.opt_str, "bar", id="opt_str-str"), + pytest.param(guards.validators.opt_str, None, id="opt_str-None"), + pytest.param(guards.validators.opt_bool, True, id="opt_bool-True"), + pytest.param(guards.validators.opt_bool, False, id="opt_bool-False"), + pytest.param(guards.validators.opt_bool, None, id="opt_bool-None"), + pytest.param(guards.validators.str_list, [], id="str_list-empty"), + pytest.param(guards.validators.str_list, ["foo"], id="str_list-onestr"), + pytest.param(guards.validators.opt_str_list, [], id="opt_str_list-empty"), + pytest.param(guards.validators.opt_str_list, ["foo"], id="opt_str_list-onestr"), + pytest.param(guards.validators.opt_str_list, None, id="opt_str_list-None"), pytest.param( - _guards.validators.opt_str_list, ["foo"], id="opt_str_list-onestr" - ), - pytest.param(_guards.validators.opt_str_list, None, id="opt_str_list-None"), - pytest.param( - _guards.validators.opt_str_list_or_commasep, + guards.validators.opt_str_list_or_commasep, [], id="opt_str_list_or_commasep-emptylist", ), pytest.param( - _guards.validators.opt_str_list_or_commasep, + guards.validators.opt_str_list_or_commasep, ["foo"], id="opt_str_list_or_commasep-list", ), pytest.param( - _guards.validators.opt_str_list_or_commasep, + guards.validators.opt_str_list_or_commasep, None, id="opt_str_list_or_commasep-None", ), @@ -134,44 +133,42 @@ def test_simple_validator_passing(validator, value): @pytest.mark.parametrize( "validator, value, match_message", ( + pytest.param(guards.validators.str_, 1, "'foo' must be a string", id="str-int"), pytest.param( - _guards.validators.str_, 1, "'foo' must be a string", id="str-int" - ), - pytest.param( - _guards.validators.str_, False, "'foo' must be a string", id="str-bool" + guards.validators.str_, False, "'foo' must be a string", id="str-bool" ), pytest.param( - _guards.validators.str_, None, "'foo' must be a string", id="str-None" + guards.validators.str_, None, "'foo' must be a string", id="str-None" ), pytest.param( - _guards.validators.int_, "bar", "'foo' must be an int", id="int-str" + guards.validators.int_, "bar", "'foo' must be an int", id="int-str" ), pytest.param( - _guards.validators.opt_str, + guards.validators.opt_str, 0, "'foo' must be a string or null", id="opt_str-int", ), pytest.param( - _guards.validators.opt_bool, + guards.validators.opt_bool, 0, "'foo' must be a bool or null", id="opt_bool-int", ), pytest.param( - _guards.validators.str_list, + guards.validators.str_list, "x", "'foo' must be a list of strings", id="str_list-str", ), pytest.param( - _guards.validators.opt_str_list, + guards.validators.opt_str_list, "x", "'foo' must be a list of strings or null", id="opt_str_list-str", ), pytest.param( - _guards.validators.opt_str_list_or_commasep, + guards.validators.opt_str_list_or_commasep, 0, "'foo' must be a list of strings or a comma-delimited string or null", id="opt_str_list_or_commasep-int", @@ -191,7 +188,7 @@ def __init__(self, *, extra=None) -> None: with pytest.raises( exc.ValidationError, match="'foo' must be a 'MyObj' object or a dictionary" ): - _guards.validators.instance_or_dict("foo", object(), MyObj) + guards.validators.instance_or_dict("foo", object(), MyObj) def test_instance_or_dict_validator_pass_on_simple_instance(): @@ -200,7 +197,7 @@ def __init__(self, *, extra=None) -> None: pass x = MyObj() - y = _guards.validators.instance_or_dict("foo", x, MyObj) + y = guards.validators.instance_or_dict("foo", x, MyObj) assert x is y @@ -209,10 +206,10 @@ class MyObj(_serializable.Serializable): def __init__(self, *, extra=None) -> None: pass - x = _guards.validators.instance_or_dict("foo", {}, MyObj) + x = guards.validators.instance_or_dict("foo", {}, MyObj) assert isinstance(x, MyObj) def test_strlist_or_commasep_splits_str(): - x = _guards.validators.opt_str_list_or_commasep("foo", "foo,bar,baz") + x = guards.validators.opt_str_list_or_commasep("foo", "foo,bar,baz") assert x == ["foo", "bar", "baz"] From b130c8c8a0642be0b2b00dcc4bf4b8376606c9e4 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 17:57:35 -0500 Subject: [PATCH 095/176] Move remarshal into _internal --- src/globus_sdk/{_remarshal.py => _internal/remarshal.py} | 0 src/globus_sdk/services/auth/client/base_login_client.py | 2 +- src/globus_sdk/services/auth/client/confidential_client.py | 2 +- src/globus_sdk/services/auth/client/service_client.py | 2 +- src/globus_sdk/services/compute/client.py | 2 +- src/globus_sdk/services/flows/client.py | 2 +- src/globus_sdk/services/gcs/client.py | 2 +- src/globus_sdk/services/gcs/data/collection.py | 2 +- src/globus_sdk/services/gcs/data/endpoint.py | 2 +- src/globus_sdk/services/gcs/data/storage_gateway.py | 2 +- src/globus_sdk/services/groups/client.py | 2 +- src/globus_sdk/services/groups/data.py | 2 +- src/globus_sdk/services/search/client.py | 2 +- src/globus_sdk/services/transfer/client.py | 2 +- src/globus_sdk/services/transfer/data/delete_data.py | 2 +- .../lazy-imports/test_modules_do_not_require_requests.py | 2 +- tests/unit/test_remarshal.py | 2 +- 17 files changed, 16 insertions(+), 16 deletions(-) rename src/globus_sdk/{_remarshal.py => _internal/remarshal.py} (100%) diff --git a/src/globus_sdk/_remarshal.py b/src/globus_sdk/_internal/remarshal.py similarity index 100% rename from src/globus_sdk/_remarshal.py rename to src/globus_sdk/_internal/remarshal.py diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index b4ec1042b..124e24191 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -7,8 +7,8 @@ from globus_sdk import client, exc from globus_sdk._internal import guards +from globus_sdk._internal.remarshal import commajoin from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer, NullAuthorizer from globus_sdk.response import GlobusHTTPResponse diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 3419c440a..a2cf1f2c2 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -4,8 +4,8 @@ import typing as t from globus_sdk import exc +from globus_sdk._internal.remarshal import commajoin, strseq_iter, strseq_listify from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin, strseq_iter, strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 67b9e3c01..45f35f885 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -6,8 +6,8 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from globus_sdk import client, exc +from globus_sdk._internal.remarshal import commajoin, strseq_listify from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin, strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.response import GlobusHTTPResponse, IterableResponse diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index 2ebff321b..435ac6814 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -4,8 +4,8 @@ import typing as t from globus_sdk import GlobusHTTPResponse, client +from globus_sdk._internal.remarshal import strseq_listify from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.scopes import ComputeScopes diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 292332fea..b874aa85c 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -13,8 +13,8 @@ paging, ) from globus_sdk._internal import guards +from globus_sdk._internal.remarshal import commajoin from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 3ec399ee7..91a055870 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -5,8 +5,8 @@ from globus_sdk import client, exc, paging, response from globus_sdk._internal.classprop import classproperty +from globus_sdk._internal.remarshal import commajoin from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk._utils import slash_join from globus_sdk.authorizers import GlobusAuthorizer diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index dd34cdc92..44eca0585 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -3,9 +3,9 @@ import abc import typing as t +from globus_sdk._internal.remarshal import strseq_listify from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload -from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from ._common import ( diff --git a/src/globus_sdk/services/gcs/data/endpoint.py b/src/globus_sdk/services/gcs/data/endpoint.py index 599d1c66e..5f0dafa6a 100644 --- a/src/globus_sdk/services/gcs/data/endpoint.py +++ b/src/globus_sdk/services/gcs/data/endpoint.py @@ -2,9 +2,9 @@ import typing as t +from globus_sdk._internal.remarshal import strseq_listify from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._remarshal import strseq_listify from globus_sdk.services.gcs.data._common import DatatypeCallback, ensure_datatype diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index 1cdbb4197..c0fcbacf2 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -3,9 +3,9 @@ import copy import typing as t +from globus_sdk._internal.remarshal import list_map, listify, strseq_listify from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload, GlobusPayload -from globus_sdk._remarshal import list_map, listify, strseq_listify from globus_sdk._types import UUIDLike from ._common import DatatypeCallback, ensure_datatype diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index 8ac13bce6..88b2afc44 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -3,8 +3,8 @@ import typing as t from globus_sdk import client, response +from globus_sdk._internal.remarshal import commajoin from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin from globus_sdk._types import UUIDLike from globus_sdk.scopes import GroupsScopes, Scope diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index cafe6251b..b33c74dfb 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -3,9 +3,9 @@ import enum import typing as t +from globus_sdk._internal.remarshal import strseq_iter from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._remarshal import strseq_iter from globus_sdk._types import UUIDLike T = t.TypeVar("T") diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index c92719844..3cedc482e 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -4,8 +4,8 @@ import typing as t from globus_sdk import client, paging, response +from globus_sdk._internal.remarshal import strseq_listify from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import strseq_listify from globus_sdk._types import UUIDLike from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import SearchScopes diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 9857735eb..d42be0a84 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -7,8 +7,8 @@ from globus_sdk import client, exc, paging, response from globus_sdk._internal import guards +from globus_sdk._internal.remarshal import commajoin from globus_sdk._missing import MISSING, MissingType -from globus_sdk._remarshal import commajoin from globus_sdk._types import DateLike, IntLike, UUIDLike from globus_sdk.scopes import GCSCollectionScopes, Scope, TransferScopes diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index 3e28825e3..f1f4631e3 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -4,9 +4,9 @@ import logging import typing as t +from globus_sdk._internal.remarshal import stringify from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._remarshal import stringify from globus_sdk._types import UUIDLike log = logging.getLogger(__name__) diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index fd70f8229..abd81d27c 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -37,8 +37,8 @@ # to ensure that the main parts (above) do not transitively pick it up "_internal.classprop", "_internal.guards", + "_internal.remarshal", "_missing", - "_remarshal", "_serializable", "_types", "_utils", diff --git a/tests/unit/test_remarshal.py b/tests/unit/test_remarshal.py index b12a2a432..86d0eeeb5 100644 --- a/tests/unit/test_remarshal.py +++ b/tests/unit/test_remarshal.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import MISSING -from globus_sdk._remarshal import ( +from globus_sdk._internal.remarshal import ( commajoin, list_map, listify, From fa1ae8085fdab38967c007e990315fe0997b2950 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 18:02:51 -0500 Subject: [PATCH 096/176] Move serializable into _internal --- src/globus_sdk/_internal/guards.py | 2 +- .../{_serializable.py => _internal/serializable.py} | 0 src/globus_sdk/gare/_auth_requirements_error.py | 2 +- src/globus_sdk/gare/_variants.py | 2 +- src/globus_sdk/token_storage/v2/token_data.py | 2 +- .../lazy-imports/test_modules_do_not_require_requests.py | 2 +- tests/unit/test_guards.py | 9 +++++---- 7 files changed, 10 insertions(+), 9 deletions(-) rename src/globus_sdk/{_serializable.py => _internal/serializable.py} (100%) diff --git a/src/globus_sdk/_internal/guards.py b/src/globus_sdk/_internal/guards.py index 674806848..a4e4d2d42 100644 --- a/src/globus_sdk/_internal/guards.py +++ b/src/globus_sdk/_internal/guards.py @@ -10,7 +10,7 @@ from globus_sdk.exc.base import ValidationError if t.TYPE_CHECKING: - from globus_sdk._serializable import Serializable + from globus_sdk._internal.serializable import Serializable if sys.version_info >= (3, 10): from typing import TypeGuard diff --git a/src/globus_sdk/_serializable.py b/src/globus_sdk/_internal/serializable.py similarity index 100% rename from src/globus_sdk/_serializable.py rename to src/globus_sdk/_internal/serializable.py diff --git a/src/globus_sdk/gare/_auth_requirements_error.py b/src/globus_sdk/gare/_auth_requirements_error.py index 281413152..7a798d2be 100644 --- a/src/globus_sdk/gare/_auth_requirements_error.py +++ b/src/globus_sdk/gare/_auth_requirements_error.py @@ -3,7 +3,7 @@ import typing as t from globus_sdk._internal.guards import validators -from globus_sdk._serializable import Serializable +from globus_sdk._internal.serializable import Serializable class GlobusAuthorizationParameters(Serializable): diff --git a/src/globus_sdk/gare/_variants.py b/src/globus_sdk/gare/_variants.py index 72b1bcdf6..3d4fc9d92 100644 --- a/src/globus_sdk/gare/_variants.py +++ b/src/globus_sdk/gare/_variants.py @@ -4,7 +4,7 @@ from globus_sdk import exc from globus_sdk._internal.guards import validators -from globus_sdk._serializable import Serializable +from globus_sdk._internal.serializable import Serializable from ._auth_requirements_error import GARE, GlobusAuthorizationParameters diff --git a/src/globus_sdk/token_storage/v2/token_data.py b/src/globus_sdk/token_storage/v2/token_data.py index 0668deac5..a428eb8d2 100644 --- a/src/globus_sdk/token_storage/v2/token_data.py +++ b/src/globus_sdk/token_storage/v2/token_data.py @@ -3,7 +3,7 @@ import typing as t from globus_sdk._internal.guards import validators -from globus_sdk._serializable import Serializable +from globus_sdk._internal.serializable import Serializable class TokenStorageData(Serializable): diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index abd81d27c..0a942b420 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -38,8 +38,8 @@ "_internal.classprop", "_internal.guards", "_internal.remarshal", + "_internal.serializable", "_missing", - "_serializable", "_types", "_utils", ), diff --git a/tests/unit/test_guards.py b/tests/unit/test_guards.py index 4efe85e63..adbf62f43 100644 --- a/tests/unit/test_guards.py +++ b/tests/unit/test_guards.py @@ -2,8 +2,9 @@ import pytest -from globus_sdk import _serializable, exc +from globus_sdk import exc from globus_sdk._internal import guards +from globus_sdk._internal.serializable import Serializable @pytest.mark.parametrize( @@ -181,7 +182,7 @@ def test_simple_validator_failing(validator, value, match_message): def test_instance_or_dict_validator_failing(): - class MyObj(_serializable.Serializable): + class MyObj(Serializable): def __init__(self, *, extra=None) -> None: pass @@ -192,7 +193,7 @@ def __init__(self, *, extra=None) -> None: def test_instance_or_dict_validator_pass_on_simple_instance(): - class MyObj(_serializable.Serializable): + class MyObj(Serializable): def __init__(self, *, extra=None) -> None: pass @@ -202,7 +203,7 @@ def __init__(self, *, extra=None) -> None: def test_instance_or_dict_validator_pass_on_simple_dict(): - class MyObj(_serializable.Serializable): + class MyObj(Serializable): def __init__(self, *, extra=None) -> None: pass From da5fc06dfc77daa18445b2fbe8afdb4e57242017 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 18:05:47 -0500 Subject: [PATCH 097/176] Move utils into _internal --- src/globus_sdk/{_utils.py => _internal/utils.py} | 0 src/globus_sdk/authorizers/access_token.py | 2 +- src/globus_sdk/authorizers/renewing.py | 2 +- src/globus_sdk/client.py | 2 +- src/globus_sdk/login_flows/command_line_login_flow_manager.py | 2 +- .../local_server_login_flow_manager.py | 2 +- .../services/auth/flow_managers/authorization_code.py | 2 +- src/globus_sdk/services/auth/flow_managers/native_app.py | 2 +- src/globus_sdk/services/gcs/client.py | 2 +- src/globus_sdk/services/timers/data.py | 2 +- src/globus_sdk/testing/models.py | 2 +- tests/common/globus_responses.py | 2 +- tests/functional/services/timers/test_jobs.py | 2 +- .../lazy-imports/test_modules_do_not_require_requests.py | 2 +- tests/unit/test_utils.py | 2 +- 15 files changed, 14 insertions(+), 14 deletions(-) rename src/globus_sdk/{_utils.py => _internal/utils.py} (100%) diff --git a/src/globus_sdk/_utils.py b/src/globus_sdk/_internal/utils.py similarity index 100% rename from src/globus_sdk/_utils.py rename to src/globus_sdk/_internal/utils.py diff --git a/src/globus_sdk/authorizers/access_token.py b/src/globus_sdk/authorizers/access_token.py index def3a2ede..a8090bebe 100644 --- a/src/globus_sdk/authorizers/access_token.py +++ b/src/globus_sdk/authorizers/access_token.py @@ -1,6 +1,6 @@ import logging -from globus_sdk._utils import sha256_string +from globus_sdk._internal.utils import sha256_string from .base import StaticGlobusAuthorizer diff --git a/src/globus_sdk/authorizers/renewing.py b/src/globus_sdk/authorizers/renewing.py index 39215ae77..f4ea3356a 100644 --- a/src/globus_sdk/authorizers/renewing.py +++ b/src/globus_sdk/authorizers/renewing.py @@ -6,7 +6,7 @@ import typing as t from globus_sdk import exc -from globus_sdk._utils import sha256_string +from globus_sdk._internal.utils import sha256_string from .base import GlobusAuthorizer diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 83312568c..90d4a192b 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -7,7 +7,7 @@ from globus_sdk import GlobusSDKUsageError, config, exc from globus_sdk._internal.classprop import classproperty -from globus_sdk._utils import slash_join +from globus_sdk._internal.utils import slash_join from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.paging import PaginatorTable from globus_sdk.response import GlobusHTTPResponse diff --git a/src/globus_sdk/login_flows/command_line_login_flow_manager.py b/src/globus_sdk/login_flows/command_line_login_flow_manager.py index 85fa92320..5fc42564b 100644 --- a/src/globus_sdk/login_flows/command_line_login_flow_manager.py +++ b/src/globus_sdk/login_flows/command_line_login_flow_manager.py @@ -5,7 +5,7 @@ from contextlib import contextmanager import globus_sdk -from globus_sdk._utils import get_nice_hostname +from globus_sdk._internal.utils import get_nice_hostname from globus_sdk.exc.base import GlobusError from globus_sdk.gare import GlobusAuthorizationParameters diff --git a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py index 3849b4370..3b5da3378 100644 --- a/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py +++ b/src/globus_sdk/login_flows/local_server_login_flow_manager/local_server_login_flow_manager.py @@ -8,7 +8,7 @@ from string import Template import globus_sdk -from globus_sdk._utils import get_nice_hostname +from globus_sdk._internal.utils import get_nice_hostname from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows.login_flow_manager import LoginFlowManager diff --git a/src/globus_sdk/services/auth/flow_managers/authorization_code.py b/src/globus_sdk/services/auth/flow_managers/authorization_code.py index af01a5e9e..9e2a62c1e 100644 --- a/src/globus_sdk/services/auth/flow_managers/authorization_code.py +++ b/src/globus_sdk/services/auth/flow_managers/authorization_code.py @@ -4,8 +4,8 @@ import typing as t import urllib.parse +from globus_sdk._internal.utils import slash_join from globus_sdk._missing import filter_missing -from globus_sdk._utils import slash_join from globus_sdk.scopes import Scope from .._common import stringify_requested_scopes diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index df88b037d..b65e68f1d 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -8,8 +8,8 @@ import typing as t import urllib.parse +from globus_sdk._internal.utils import slash_join from globus_sdk._missing import MISSING, MissingType, filter_missing -from globus_sdk._utils import slash_join from globus_sdk.exc import GlobusSDKUsageError from globus_sdk.scopes import Scope diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 91a055870..7e49017b8 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -6,9 +6,9 @@ from globus_sdk import client, exc, paging, response from globus_sdk._internal.classprop import classproperty from globus_sdk._internal.remarshal import commajoin +from globus_sdk._internal.utils import slash_join from globus_sdk._missing import MISSING, MissingType from globus_sdk._types import UUIDLike -from globus_sdk._utils import slash_join from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import GCSCollectionScopes, GCSEndpointScopes, Scope diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index 09258f29c..18a9f6b4c 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -6,9 +6,9 @@ import logging import typing as t +from globus_sdk._internal.utils import slash_join from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._utils import slash_join from globus_sdk.config import get_service_url from globus_sdk.exc import warn_deprecated from globus_sdk.services.transfer import TransferData diff --git a/src/globus_sdk/testing/models.py b/src/globus_sdk/testing/models.py index d5536153f..0bef17111 100644 --- a/src/globus_sdk/testing/models.py +++ b/src/globus_sdk/testing/models.py @@ -5,7 +5,7 @@ import responses -from globus_sdk._utils import slash_join +from globus_sdk._internal.utils import slash_join class RegisteredResponse: diff --git a/tests/common/globus_responses.py b/tests/common/globus_responses.py index a3c67e1f5..e8e7bfd37 100644 --- a/tests/common/globus_responses.py +++ b/tests/common/globus_responses.py @@ -3,7 +3,7 @@ import responses -from globus_sdk._utils import slash_join +from globus_sdk._internal.utils import slash_join def register_api_route_fixture_file(service, path, filename, **kwargs): diff --git a/tests/functional/services/timers/test_jobs.py b/tests/functional/services/timers/test_jobs.py index 973385a0a..b2d18b989 100644 --- a/tests/functional/services/timers/test_jobs.py +++ b/tests/functional/services/timers/test_jobs.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import TimerJob, TimersAPIError, TransferData, config, exc -from globus_sdk._utils import slash_join +from globus_sdk._internal.utils import slash_join from globus_sdk.testing import get_last_request, load_response from tests.common import GO_EP1_ID, GO_EP2_ID diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index 0a942b420..5381bf514 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -39,9 +39,9 @@ "_internal.guards", "_internal.remarshal", "_internal.serializable", + "_internal.utils", "_missing", "_types", - "_utils", ), ) def test_module_does_not_require_requests(module_name): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 2e78a16f8..19cfb7637 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk._utils import get_nice_hostname, sha256_string, slash_join +from globus_sdk._internal.utils import get_nice_hostname, sha256_string, slash_join def test_sha256string(): From b38494daa0f0120a5abd27da2a635b936ddc5204 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Jul 2025 18:15:25 -0500 Subject: [PATCH 098/176] Move type definitions into _internal Also rename from `_types` (generic, unclear) to `type_definitions` (still generic, hopefully less unclear). --- src/globus_sdk/_internal/guards.py | 2 +- src/globus_sdk/{_types.py => _internal/type_definitions.py} | 0 src/globus_sdk/globus_app/app.py | 2 +- src/globus_sdk/globus_app/client_app.py | 2 +- src/globus_sdk/globus_app/protocols.py | 2 +- src/globus_sdk/globus_app/user_app.py | 2 +- src/globus_sdk/scopes/consents/_model.py | 2 +- src/globus_sdk/scopes/data/flows.py | 2 +- src/globus_sdk/services/auth/client/base_login_client.py | 2 +- src/globus_sdk/services/auth/client/confidential_client.py | 2 +- src/globus_sdk/services/auth/client/native_client.py | 2 +- src/globus_sdk/services/auth/client/service_client.py | 2 +- src/globus_sdk/services/auth/data.py | 2 +- src/globus_sdk/services/compute/client.py | 2 +- src/globus_sdk/services/compute/data.py | 2 +- src/globus_sdk/services/flows/client.py | 2 +- src/globus_sdk/services/gcs/client.py | 2 +- src/globus_sdk/services/gcs/connector_table.py | 2 +- src/globus_sdk/services/gcs/data/collection.py | 2 +- src/globus_sdk/services/gcs/data/role.py | 2 +- src/globus_sdk/services/gcs/data/storage_gateway.py | 2 +- src/globus_sdk/services/gcs/data/user_credential.py | 2 +- src/globus_sdk/services/groups/client.py | 2 +- src/globus_sdk/services/groups/data.py | 2 +- src/globus_sdk/services/groups/manager.py | 2 +- src/globus_sdk/services/search/client.py | 2 +- src/globus_sdk/services/timers/client.py | 2 +- src/globus_sdk/services/transfer/client.py | 2 +- src/globus_sdk/services/transfer/data/delete_data.py | 2 +- src/globus_sdk/services/transfer/data/transfer_data.py | 2 +- src/globus_sdk/token_storage/v2/base.py | 2 +- src/globus_sdk/token_storage/v2/memory.py | 2 +- .../token_storage/v2/validating_token_storage/errors.py | 2 +- tests/common/consents.py | 2 +- .../lazy-imports/test_modules_do_not_require_requests.py | 2 +- tests/non-pytest/mypy-ignore-tests/responselike_protocol.py | 2 +- tests/unit/helpers/gcs/test_collections.py | 2 +- 37 files changed, 36 insertions(+), 36 deletions(-) rename src/globus_sdk/{_types.py => _internal/type_definitions.py} (100%) diff --git a/src/globus_sdk/_internal/guards.py b/src/globus_sdk/_internal/guards.py index a4e4d2d42..4a7d3cc2a 100644 --- a/src/globus_sdk/_internal/guards.py +++ b/src/globus_sdk/_internal/guards.py @@ -4,7 +4,7 @@ import typing as t import uuid -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike # some error types use guards, so import from the specific module to avoid circularity from globus_sdk.exc.base import ValidationError diff --git a/src/globus_sdk/_types.py b/src/globus_sdk/_internal/type_definitions.py similarity index 100% rename from src/globus_sdk/_types.py rename to src/globus_sdk/_internal/type_definitions.py diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index 72e5f6d9b..103001438 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -11,7 +11,7 @@ GlobusSDKUsageError, IDTokenDecoder, ) -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions 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 diff --git a/src/globus_sdk/globus_app/client_app.py b/src/globus_sdk/globus_app/client_app.py index 8c8f9fe0e..e5148ab84 100644 --- a/src/globus_sdk/globus_app/client_app.py +++ b/src/globus_sdk/globus_app/client_app.py @@ -3,7 +3,7 @@ import typing as t from globus_sdk import AuthLoginClient, ConfidentialAppAuthClient, GlobusSDKUsageError -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.scopes import Scope diff --git a/src/globus_sdk/globus_app/protocols.py b/src/globus_sdk/globus_app/protocols.py index 02f0e3d1f..eac88306f 100644 --- a/src/globus_sdk/globus_app/protocols.py +++ b/src/globus_sdk/globus_app/protocols.py @@ -4,7 +4,7 @@ if t.TYPE_CHECKING: from globus_sdk import AuthLoginClient, IDTokenDecoder - from globus_sdk._types import UUIDLike + from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.login_flows import LoginFlowManager from globus_sdk.token_storage import TokenStorage, TokenValidationError diff --git a/src/globus_sdk/globus_app/user_app.py b/src/globus_sdk/globus_app/user_app.py index 5de12f977..bfe31f0cd 100644 --- a/src/globus_sdk/globus_app/user_app.py +++ b/src/globus_sdk/globus_app/user_app.py @@ -10,7 +10,7 @@ NativeAppAuthClient, Scope, ) -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows import CommandLineLoginFlowManager, LoginFlowManager from globus_sdk.token_storage import ( diff --git a/src/globus_sdk/scopes/consents/_model.py b/src/globus_sdk/scopes/consents/_model.py index a5009bf6f..412f8a79b 100644 --- a/src/globus_sdk/scopes/consents/_model.py +++ b/src/globus_sdk/scopes/consents/_model.py @@ -30,7 +30,7 @@ from dataclasses import dataclass from datetime import datetime -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from ..parser import ScopeParser from ..representation import Scope diff --git a/src/globus_sdk/scopes/data/flows.py b/src/globus_sdk/scopes/data/flows.py index 7d698137e..7c62bb4a6 100644 --- a/src/globus_sdk/scopes/data/flows.py +++ b/src/globus_sdk/scopes/data/flows.py @@ -2,7 +2,7 @@ import typing as t -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from ..collection import ( DynamicScopeCollection, diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 124e24191..1ad68f805 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -8,8 +8,8 @@ from globus_sdk import client, exc from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer, NullAuthorizer from globus_sdk.response import GlobusHTTPResponse from globus_sdk.scopes import AuthScopes, Scope diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index a2cf1f2c2..c7de4d826 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -5,8 +5,8 @@ from globus_sdk import exc from globus_sdk._internal.remarshal import commajoin, strseq_iter, strseq_listify +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse from globus_sdk.scopes import Scope diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index 996c761a8..790ff91cf 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -3,8 +3,8 @@ import logging import typing as t +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.authorizers import NullAuthorizer from globus_sdk.response import GlobusHTTPResponse from globus_sdk.scopes import Scope diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 45f35f885..34094006b 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -7,8 +7,8 @@ from globus_sdk import client, exc from globus_sdk._internal.remarshal import commajoin, strseq_listify +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.response import GlobusHTTPResponse, IterableResponse from globus_sdk.scopes import AuthScopes, Scope diff --git a/src/globus_sdk/services/auth/data.py b/src/globus_sdk/services/auth/data.py index 21c496cb0..76b9fac7e 100644 --- a/src/globus_sdk/services/auth/data.py +++ b/src/globus_sdk/services/auth/data.py @@ -1,5 +1,5 @@ +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._payload import GlobusPayload -from globus_sdk._types import UUIDLike class DependentScopeSpec(GlobusPayload): diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index 435ac6814..be470362f 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -5,8 +5,8 @@ from globus_sdk import GlobusHTTPResponse, client from globus_sdk._internal.remarshal import strseq_listify +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.scopes import ComputeScopes from .errors import ComputeAPIError diff --git a/src/globus_sdk/services/compute/data.py b/src/globus_sdk/services/compute/data.py index 7ccd7557d..ce9e9398b 100644 --- a/src/globus_sdk/services/compute/data.py +++ b/src/globus_sdk/services/compute/data.py @@ -1,8 +1,8 @@ from __future__ import annotations +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._types import UUIDLike from globus_sdk.exc import warn_deprecated diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index b874aa85c..1318067e1 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -14,8 +14,8 @@ ) from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import ( diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 7e49017b8..121bd50bd 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -6,9 +6,9 @@ from globus_sdk import client, exc, paging, response from globus_sdk._internal.classprop import classproperty from globus_sdk._internal.remarshal import commajoin +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._internal.utils import slash_join from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import GCSCollectionScopes, GCSEndpointScopes, Scope diff --git a/src/globus_sdk/services/gcs/connector_table.py b/src/globus_sdk/services/gcs/connector_table.py index 5c3928f1d..3e8a66ad1 100644 --- a/src/globus_sdk/services/gcs/connector_table.py +++ b/src/globus_sdk/services/gcs/connector_table.py @@ -4,7 +4,7 @@ import re import typing as t -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike _NORMALIZATION_PATTERN = re.compile(r"[_\- ]+") diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index 44eca0585..99130a0be 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -4,9 +4,9 @@ import typing as t from globus_sdk._internal.remarshal import strseq_listify +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload -from globus_sdk._types import UUIDLike from ._common import ( DatatypeCallback, diff --git a/src/globus_sdk/services/gcs/data/role.py b/src/globus_sdk/services/gcs/data/role.py index b1ce6ca5e..4dd6e726b 100644 --- a/src/globus_sdk/services/gcs/data/role.py +++ b/src/globus_sdk/services/gcs/data/role.py @@ -2,9 +2,9 @@ import typing as t +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._types import UUIDLike class GCSRoleDocument(GlobusPayload): diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index c0fcbacf2..b373fafec 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -4,9 +4,9 @@ import typing as t from globus_sdk._internal.remarshal import list_map, listify, strseq_listify +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload, GlobusPayload -from globus_sdk._types import UUIDLike from ._common import DatatypeCallback, ensure_datatype diff --git a/src/globus_sdk/services/gcs/data/user_credential.py b/src/globus_sdk/services/gcs/data/user_credential.py index d625ada90..4b228a7f7 100644 --- a/src/globus_sdk/services/gcs/data/user_credential.py +++ b/src/globus_sdk/services/gcs/data/user_credential.py @@ -2,9 +2,9 @@ import typing as t +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._types import UUIDLike class UserCredentialDocument(GlobusPayload): diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index 88b2afc44..53aecf5c4 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -4,8 +4,8 @@ from globus_sdk import client, response from globus_sdk._internal.remarshal import commajoin +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.scopes import GroupsScopes, Scope from .data import BatchMembershipActions, GroupPolicies diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index b33c74dfb..21605b096 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -4,9 +4,9 @@ import typing as t from globus_sdk._internal.remarshal import strseq_iter +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._types import UUIDLike T = t.TypeVar("T") diff --git a/src/globus_sdk/services/groups/manager.py b/src/globus_sdk/services/groups/manager.py index 5521e1c7a..fa3ecf410 100644 --- a/src/globus_sdk/services/groups/manager.py +++ b/src/globus_sdk/services/groups/manager.py @@ -3,7 +3,7 @@ import typing as t from globus_sdk import response -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from .client import GroupsClient from .data import ( diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index 3cedc482e..c29a426c3 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -5,8 +5,8 @@ from globus_sdk import client, paging, response from globus_sdk._internal.remarshal import strseq_listify +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import UUIDLike from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import SearchScopes diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index 270f7d5d6..9d153287f 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -6,7 +6,7 @@ from globus_sdk import client, exc, response from globus_sdk._internal import guards -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.scopes import ( GCSCollectionScopes, Scope, diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index d42be0a84..9aba4ef0a 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -8,8 +8,8 @@ from globus_sdk import client, exc, paging, response from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin +from globus_sdk._internal.type_definitions import DateLike, IntLike, UUIDLike from globus_sdk._missing import MISSING, MissingType -from globus_sdk._types import DateLike, IntLike, UUIDLike from globus_sdk.scopes import GCSCollectionScopes, Scope, TransferScopes from .data import DeleteData, TransferData diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index f1f4631e3..ca5fbdedf 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -5,9 +5,9 @@ import typing as t from globus_sdk._internal.remarshal import stringify +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._types import UUIDLike log = logging.getLogger(__name__) diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index bf9b52289..55dc99313 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -4,9 +4,9 @@ import logging import typing as t +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk._types import UUIDLike log = logging.getLogger(__name__) _sync_level_dict: dict[t.Literal["exists", "size", "mtime", "checksum"], int] = { diff --git a/src/globus_sdk/token_storage/v2/base.py b/src/globus_sdk/token_storage/v2/base.py index abcb4e9b7..abee7d166 100644 --- a/src/globus_sdk/token_storage/v2/base.py +++ b/src/globus_sdk/token_storage/v2/base.py @@ -9,7 +9,7 @@ import typing as t import globus_sdk -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from .token_data import TokenStorageData diff --git a/src/globus_sdk/token_storage/v2/memory.py b/src/globus_sdk/token_storage/v2/memory.py index 50eb27d09..30b02bed5 100644 --- a/src/globus_sdk/token_storage/v2/memory.py +++ b/src/globus_sdk/token_storage/v2/memory.py @@ -6,7 +6,7 @@ from .token_data import TokenStorageData if t.TYPE_CHECKING: - from globus_sdk._types import UUIDLike + from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.globus_app import GlobusAppConfig diff --git a/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py b/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py index 3fb0d45d3..fbd2349cb 100644 --- a/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py +++ b/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py @@ -3,7 +3,7 @@ from datetime import datetime from globus_sdk import GlobusError, Scope -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike class TokenValidationError(GlobusError): diff --git a/tests/common/consents.py b/tests/common/consents.py index e05f25bd0..62f8bf3c3 100644 --- a/tests/common/consents.py +++ b/tests/common/consents.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta -from globus_sdk._types import UUIDLike +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.scopes import Scope, ScopeParser from globus_sdk.scopes.consents import Consent, ConsentForest diff --git a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py index 5381bf514..a006deb09 100644 --- a/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py +++ b/tests/non-pytest/lazy-imports/test_modules_do_not_require_requests.py @@ -40,8 +40,8 @@ "_internal.remarshal", "_internal.serializable", "_internal.utils", + "_internal.type_definitions", "_missing", - "_types", ), ) def test_module_does_not_require_requests(module_name): diff --git a/tests/non-pytest/mypy-ignore-tests/responselike_protocol.py b/tests/non-pytest/mypy-ignore-tests/responselike_protocol.py index 0c90df3ba..136c9ba0a 100644 --- a/tests/non-pytest/mypy-ignore-tests/responselike_protocol.py +++ b/tests/non-pytest/mypy-ignore-tests/responselike_protocol.py @@ -7,7 +7,7 @@ import requests from globus_sdk import GlobusAPIError, GlobusHTTPResponse -from globus_sdk._types import ResponseLike +from globus_sdk._internal.type_definitions import ResponseLike if sys.version_info < (3, 11): from typing_extensions import assert_type diff --git a/tests/unit/helpers/gcs/test_collections.py b/tests/unit/helpers/gcs/test_collections.py index 0bd1f4775..6970988b7 100644 --- a/tests/unit/helpers/gcs/test_collections.py +++ b/tests/unit/helpers/gcs/test_collections.py @@ -12,8 +12,8 @@ POSIXCollectionPolicies, POSIXStagingCollectionPolicies, ) +from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType, filter_missing -from globus_sdk._types import UUIDLike from globus_sdk.transport import JSONRequestEncoder STUB_SG_ID = uuid.uuid1() # storage gateway From 3eddb611da8bc8dc6e2a24a7f6112746d48b3241 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 11 Jul 2025 14:36:51 -0500 Subject: [PATCH 099/176] Remove the `UUIDLike` type alias This is not strictly necessary and importantly it isn't interpreted as we'd like by Sphinx, so users are seeing the `UUIDLike` name in our docs instead of the union. Use `Union` rather than `UnionType` in tests which have a runtime context. --- src/globus_sdk/_internal/guards.py | 6 +- src/globus_sdk/_internal/type_definitions.py | 2 - src/globus_sdk/globus_app/app.py | 12 +- src/globus_sdk/globus_app/client_app.py | 6 +- src/globus_sdk/globus_app/protocols.py | 4 +- src/globus_sdk/globus_app/user_app.py | 6 +- src/globus_sdk/scopes/consents/_model.py | 9 +- src/globus_sdk/scopes/data/flows.py | 5 +- .../services/auth/client/base_login_client.py | 8 +- .../auth/client/confidential_client.py | 10 +- .../services/auth/client/native_client.py | 6 +- .../services/auth/client/service_client.py | 78 +++++----- src/globus_sdk/services/auth/data.py | 7 +- src/globus_sdk/services/compute/client.py | 30 ++-- src/globus_sdk/services/compute/data.py | 5 +- src/globus_sdk/services/flows/client.py | 33 ++--- src/globus_sdk/services/gcs/client.py | 29 ++-- .../services/gcs/connector_table.py | 7 +- .../services/gcs/data/collection.py | 16 +-- src/globus_sdk/services/gcs/data/role.py | 4 +- .../services/gcs/data/storage_gateway.py | 4 +- .../services/gcs/data/user_credential.py | 8 +- src/globus_sdk/services/groups/client.py | 20 +-- src/globus_sdk/services/groups/data.py | 24 ++-- src/globus_sdk/services/groups/manager.py | 30 ++-- src/globus_sdk/services/search/client.py | 42 +++--- src/globus_sdk/services/timers/client.py | 13 +- src/globus_sdk/services/transfer/client.py | 136 +++++++++--------- .../services/transfer/data/delete_data.py | 6 +- .../services/transfer/data/transfer_data.py | 8 +- src/globus_sdk/token_storage/v2/base.py | 6 +- src/globus_sdk/token_storage/v2/memory.py | 4 +- .../v2/validating_token_storage/errors.py | 6 +- tests/common/consents.py | 7 +- tests/unit/helpers/gcs/test_collections.py | 11 +- 35 files changed, 311 insertions(+), 297 deletions(-) diff --git a/src/globus_sdk/_internal/guards.py b/src/globus_sdk/_internal/guards.py index 4a7d3cc2a..bd4d701cd 100644 --- a/src/globus_sdk/_internal/guards.py +++ b/src/globus_sdk/_internal/guards.py @@ -4,8 +4,6 @@ import typing as t import uuid -from globus_sdk._internal.type_definitions import UUIDLike - # some error types use guards, so import from the specific module to avoid circularity from globus_sdk.exc.base import ValidationError @@ -94,7 +92,7 @@ def instance_or_dict(name: str, value: t.Any, cls: type[S]) -> S: ) @staticmethod - def uuidlike(name: str, s: t.Any) -> UUIDLike: + def uuidlike(name: str, s: t.Any) -> uuid.UUID | str: """ Raise an error if the input is not a UUID @@ -108,7 +106,7 @@ def uuidlike(name: str, s: t.Any) -> UUIDLike: .. code-block:: python - def frob_it(collection_id: UUIDLike) -> Frob: + def frob_it(collection_id: uuid.UUID | str) -> Frob: validators.uuidlike(collection_id, name="collection_id") return Frob(collection_id) diff --git a/src/globus_sdk/_internal/type_definitions.py b/src/globus_sdk/_internal/type_definitions.py index c2a1d1997..b17b7cc31 100644 --- a/src/globus_sdk/_internal/type_definitions.py +++ b/src/globus_sdk/_internal/type_definitions.py @@ -2,11 +2,9 @@ import datetime import typing as t -import uuid # 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] diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index 103001438..408dbb0b8 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -4,6 +4,7 @@ import contextlib import copy import typing as t +import uuid from globus_sdk import ( AuthClient, @@ -11,7 +12,6 @@ GlobusSDKUsageError, IDTokenDecoder, ) -from globus_sdk._internal.type_definitions 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 @@ -64,7 +64,7 @@ def __init__( app_name: str = "Unnamed Globus App", *, login_client: AuthLoginClient | None = None, - client_id: UUIDLike | None = None, + client_id: uuid.UUID | str | None = None, client_secret: str | None = None, scope_requirements: ( t.Mapping[str, str | Scope | t.Iterable[str | Scope]] | None @@ -139,9 +139,9 @@ def _resolve_client_info( app_name: str, config: GlobusAppConfig, login_client: AuthLoginClient | None, - client_id: UUIDLike | None, + client_id: uuid.UUID | str | None, client_secret: str | None, - ) -> tuple[UUIDLike, AuthLoginClient]: + ) -> tuple[uuid.UUID | str, AuthLoginClient]: """ Extracts a client_id and login_client from GlobusApp initialization parameters, validating that the parameters were provided correctly. @@ -192,7 +192,7 @@ def _initialize_login_client( self, app_name: str, config: GlobusAppConfig, - client_id: UUIDLike, + client_id: uuid.UUID | str, client_secret: str | None, ) -> AuthLoginClient: """ @@ -220,7 +220,7 @@ def _initialize_validating_token_storage( return validating_token_storage def _resolve_token_storage( - self, app_name: str, client_id: UUIDLike, config: GlobusAppConfig + self, app_name: str, client_id: uuid.UUID | str, config: GlobusAppConfig ) -> TokenStorage: """ Resolve the raw token storage to be used by the app. diff --git a/src/globus_sdk/globus_app/client_app.py b/src/globus_sdk/globus_app/client_app.py index e5148ab84..4718099c0 100644 --- a/src/globus_sdk/globus_app/client_app.py +++ b/src/globus_sdk/globus_app/client_app.py @@ -1,9 +1,9 @@ from __future__ import annotations import typing as t +import uuid from globus_sdk import AuthLoginClient, ConfidentialAppAuthClient, GlobusSDKUsageError -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.scopes import Scope @@ -58,7 +58,7 @@ def __init__( app_name: str = "Unnamed Globus App", *, login_client: ConfidentialAppAuthClient | None = None, - client_id: UUIDLike | None = None, + client_id: uuid.UUID | str | None = None, client_secret: str | None = None, scope_requirements: ( dict[str, str | Scope | t.Iterable[str | Scope]] | None @@ -86,7 +86,7 @@ def _initialize_login_client( self, app_name: str, config: GlobusAppConfig, - client_id: UUIDLike, + client_id: uuid.UUID | str, client_secret: str | None, ) -> AuthLoginClient: if not client_secret: diff --git a/src/globus_sdk/globus_app/protocols.py b/src/globus_sdk/globus_app/protocols.py index eac88306f..615aa963b 100644 --- a/src/globus_sdk/globus_app/protocols.py +++ b/src/globus_sdk/globus_app/protocols.py @@ -1,10 +1,10 @@ from __future__ import annotations import typing as t +import uuid if t.TYPE_CHECKING: from globus_sdk import AuthLoginClient, IDTokenDecoder - from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.login_flows import LoginFlowManager from globus_sdk.token_storage import TokenStorage, TokenValidationError @@ -26,7 +26,7 @@ def for_globus_app( *, app_name: str, config: GlobusAppConfig, - client_id: UUIDLike, + client_id: uuid.UUID | str, namespace: str, ) -> TokenStorage: """ diff --git a/src/globus_sdk/globus_app/user_app.py b/src/globus_sdk/globus_app/user_app.py index bfe31f0cd..f7bd4731f 100644 --- a/src/globus_sdk/globus_app/user_app.py +++ b/src/globus_sdk/globus_app/user_app.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing as t +import uuid from globus_sdk import ( AuthClient, @@ -10,7 +11,6 @@ NativeAppAuthClient, Scope, ) -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.gare import GlobusAuthorizationParameters from globus_sdk.login_flows import CommandLineLoginFlowManager, LoginFlowManager from globus_sdk.token_storage import ( @@ -78,7 +78,7 @@ def __init__( app_name: str = "Unnamed Globus App", *, login_client: AuthLoginClient | None = None, - client_id: UUIDLike | None = None, + client_id: uuid.UUID | str | None = None, client_secret: str | None = None, scope_requirements: ( t.Mapping[str, str | Scope | t.Iterable[str | Scope]] | None @@ -129,7 +129,7 @@ def _initialize_login_client( self, app_name: str, config: GlobusAppConfig, - client_id: UUIDLike, + client_id: uuid.UUID | str, client_secret: str | None, ) -> AuthLoginClient: if client_secret: diff --git a/src/globus_sdk/scopes/consents/_model.py b/src/globus_sdk/scopes/consents/_model.py index 412f8a79b..07d454cbd 100644 --- a/src/globus_sdk/scopes/consents/_model.py +++ b/src/globus_sdk/scopes/consents/_model.py @@ -27,11 +27,10 @@ import textwrap import typing as t +import uuid from dataclasses import dataclass from datetime import datetime -from globus_sdk._internal.type_definitions import UUIDLike - from ..parser import ScopeParser from ..representation import Scope from ._errors import ConsentParseError, ConsentTreeConstructionError @@ -50,11 +49,11 @@ class Consent: operations (consents) defined in the "dependency_path". """ - client: UUIDLike - scope: UUIDLike + client: uuid.UUID | str + scope: uuid.UUID | str scope_name: str id: int - effective_identity: UUIDLike + effective_identity: uuid.UUID | str # A list representing the path of consent dependencies leading from a "root consent" # to this. The last element of this list will always be this consent's ID. # Downstream dependency relationships may exist but will not be defined here. diff --git a/src/globus_sdk/scopes/data/flows.py b/src/globus_sdk/scopes/data/flows.py index 7c62bb4a6..c0f8dc9f8 100644 --- a/src/globus_sdk/scopes/data/flows.py +++ b/src/globus_sdk/scopes/data/flows.py @@ -1,8 +1,7 @@ from __future__ import annotations import typing as t - -from globus_sdk._internal.type_definitions import UUIDLike +import uuid from ..collection import ( DynamicScopeCollection, @@ -47,7 +46,7 @@ class SpecificFlowScopes(DynamicScopeCollection): _scope_names = ("user",) - def __init__(self, flow_id: UUIDLike) -> None: + def __init__(self, flow_id: uuid.UUID | str) -> None: _flow_id = str(flow_id) super().__init__(_flow_id) diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 1ad68f805..06b2d5973 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -2,13 +2,13 @@ import logging import typing as t +import uuid from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from globus_sdk import client, exc from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import GlobusAuthorizer, NullAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -48,7 +48,7 @@ class AuthLoginClient(client.BaseClient): def __init__( self, - client_id: UUIDLike | None = None, + client_id: uuid.UUID | str | None = None, environment: str | None = None, base_url: str | None = None, authorizer: GlobusAuthorizer | None = None, @@ -147,11 +147,11 @@ def oauth2_get_authorize_url( self, *, session_required_identities: ( - UUIDLike | t.Iterable[UUIDLike] | MissingType + uuid.UUID | str | t.Iterable[uuid.UUID | str] | MissingType ) = MISSING, session_required_single_domain: str | t.Iterable[str] | MissingType = MISSING, session_required_policies: ( - UUIDLike | t.Iterable[UUIDLike] | MissingType + uuid.UUID | str | t.Iterable[uuid.UUID | str] | MissingType ) = MISSING, session_required_mfa: bool | MissingType = MISSING, session_message: str | MissingType = MISSING, diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index c7de4d826..35b9d683c 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -2,10 +2,10 @@ import logging import typing as t +import uuid from globus_sdk import exc from globus_sdk._internal.remarshal import commajoin, strseq_iter, strseq_listify -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -47,7 +47,7 @@ class ConfidentialAppAuthClient(AuthLoginClient): def __init__( self, - client_id: UUIDLike, + client_id: uuid.UUID | str, client_secret: str, environment: str | None = None, base_url: str | None = None, @@ -67,7 +67,7 @@ def get_identities( self, *, usernames: t.Iterable[str] | str | MissingType = MISSING, - ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, + ids: t.Iterable[uuid.UUID | str] | uuid.UUID | str | MissingType = MISSING, provision: bool = False, query_params: dict[str, t.Any] | None = None, ) -> GetIdentitiesResponse: @@ -345,8 +345,8 @@ def create_child_client( redirect_uris: t.Iterable[str] | MissingType = MISSING, terms_and_conditions: str | MissingType = MISSING, privacy_policy: str | MissingType = MISSING, - required_idp: UUIDLike | MissingType = MISSING, - preselect_idp: UUIDLike | MissingType = MISSING, + required_idp: uuid.UUID | str | MissingType = MISSING, + preselect_idp: uuid.UUID | str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index 790ff91cf..d801f3b69 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -2,8 +2,8 @@ import logging import typing as t +import uuid -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import NullAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -35,7 +35,7 @@ class NativeAppAuthClient(AuthLoginClient): def __init__( self, - client_id: UUIDLike, + client_id: uuid.UUID | str, environment: str | None = None, base_url: str | None = None, app_name: str | None = None, @@ -139,7 +139,7 @@ def oauth2_refresh_token( def create_native_app_instance( self, - template_id: UUIDLike, + template_id: uuid.UUID | str, name: str, ) -> GlobusHTTPResponse: """ diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index 34094006b..ca1b2b5f6 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -2,12 +2,12 @@ import logging import typing as t +import uuid from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from globus_sdk import client, exc from globus_sdk._internal.remarshal import commajoin, strseq_listify -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.response import GlobusHTTPResponse, IterableResponse @@ -72,7 +72,7 @@ class AuthClient(client.BaseClient): def __init__( self, - client_id: UUIDLike | None = None, + client_id: uuid.UUID | str | None = None, environment: str | None = None, base_url: str | None = None, app: GlobusApp | None = None, @@ -113,7 +113,7 @@ def client_id(self) -> str | None: return self._client_id @client_id.setter - def client_id(self, value: UUIDLike) -> None: + def client_id(self, value: uuid.UUID | str) -> None: exc.warn_deprecated( "The client_id attribute on `AuthClient` / " "`AuthClient` is deprecated. " @@ -226,7 +226,7 @@ def get_identities( self, *, usernames: t.Iterable[str] | str | MissingType = MISSING, - ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, + ids: t.Iterable[uuid.UUID | str] | uuid.UUID | str | MissingType = MISSING, provision: bool = False, query_params: dict[str, t.Any] | None = None, ) -> GetIdentitiesResponse: @@ -340,7 +340,7 @@ def get_identity_providers( self, *, domains: t.Iterable[str] | str | MissingType = MISSING, - ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, + ids: t.Iterable[uuid.UUID | str] | uuid.UUID | str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> GetIdentityProvidersResponse: r""" @@ -431,7 +431,7 @@ def get_identity_providers( # Developer APIs # - def get_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: + def get_project(self, project_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Look up a project. Requires the ``manage_projects`` scope. @@ -532,8 +532,12 @@ def create_project( display_name: str, contact_email: str, *, - admin_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, - admin_group_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, + admin_ids: ( + uuid.UUID | str | t.Iterable[uuid.UUID | str] | MissingType + ) = MISSING, + admin_group_ids: ( + uuid.UUID | str | t.Iterable[uuid.UUID | str] | MissingType + ) = MISSING, ) -> GlobusHTTPResponse: """ Create a new project. Requires the ``manage_projects`` scope. @@ -588,12 +592,16 @@ def create_project( def update_project( self, - project_id: UUIDLike, + project_id: uuid.UUID | str, *, display_name: str | MissingType = MISSING, contact_email: str | MissingType = MISSING, - admin_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, - admin_group_ids: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, + admin_ids: ( + uuid.UUID | str | t.Iterable[uuid.UUID | str] | MissingType + ) = MISSING, + admin_group_ids: ( + uuid.UUID | str | t.Iterable[uuid.UUID | str] | MissingType + ) = MISSING, ) -> GlobusHTTPResponse: """ Update a project. Requires the ``manage_projects`` scope. @@ -639,7 +647,7 @@ def update_project( } return self.put(f"/v2/api/projects/{project_id}", data={"project": body}) - def delete_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: + def delete_project(self, project_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Delete a project. Requires the ``manage_projects`` scope. @@ -668,7 +676,7 @@ def delete_project(self, project_id: UUIDLike) -> GlobusHTTPResponse: """ return self.delete(f"/v2/api/projects/{project_id}") - def get_policy(self, policy_id: UUIDLike) -> GlobusHTTPResponse: + def get_policy(self, policy_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Look up a policy. Requires the ``manage_projects`` scope. @@ -767,7 +775,7 @@ def get_policies(self) -> IterableResponse: def create_policy( self, *, - project_id: UUIDLike, + project_id: uuid.UUID | str, display_name: str, description: str, high_assurance: bool | MissingType = MISSING, @@ -846,9 +854,9 @@ def create_policy( def update_policy( self, - policy_id: UUIDLike, + policy_id: uuid.UUID | str, *, - project_id: UUIDLike | MissingType = MISSING, + project_id: uuid.UUID | str | MissingType = MISSING, authentication_assurance_timeout: int | MissingType = MISSING, required_mfa: bool | MissingType = MISSING, display_name: str | MissingType = MISSING, @@ -905,7 +913,7 @@ def update_policy( } return self.put(f"/v2/api/policies/{policy_id}", data={"policy": body}) - def delete_policy(self, policy_id: UUIDLike) -> GlobusHTTPResponse: + def delete_policy(self, policy_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Delete a policy. Requires the ``manage_projects`` scope. @@ -937,7 +945,7 @@ def delete_policy(self, policy_id: UUIDLike) -> GlobusHTTPResponse: def get_client( self, *, - client_id: UUIDLike | MissingType = MISSING, + client_id: uuid.UUID | str | MissingType = MISSING, fqdn: str | MissingType = MISSING, ) -> GlobusHTTPResponse: """ @@ -1086,7 +1094,7 @@ def get_clients(self) -> IterableResponse: def create_client( self, name: str, - project: UUIDLike, + project: uuid.UUID | str, *, public_client: bool | MissingType = MISSING, client_type: ( @@ -1104,8 +1112,8 @@ def create_client( redirect_uris: t.Iterable[str] | MissingType = MISSING, terms_and_conditions: str | MissingType = MISSING, privacy_policy: str | MissingType = MISSING, - required_idp: UUIDLike | MissingType = MISSING, - preselect_idp: UUIDLike | MissingType = MISSING, + required_idp: uuid.UUID | str | MissingType = MISSING, + preselect_idp: uuid.UUID | str | MissingType = MISSING, additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> GlobusHTTPResponse: """ @@ -1234,15 +1242,15 @@ def create_client( def update_client( self, - client_id: UUIDLike, + client_id: uuid.UUID | str, *, name: str | MissingType = MISSING, visibility: MissingType | t.Literal["public", "private"] = MISSING, redirect_uris: t.Iterable[str] | MissingType = MISSING, terms_and_conditions: str | None | MissingType = MISSING, privacy_policy: str | None | MissingType = MISSING, - required_idp: UUIDLike | None | MissingType = MISSING, - preselect_idp: UUIDLike | None | MissingType = MISSING, + required_idp: uuid.UUID | str | None | MissingType = MISSING, + preselect_idp: uuid.UUID | str | None | MissingType = MISSING, additional_fields: dict[str, t.Any] | MissingType = MISSING, ) -> GlobusHTTPResponse: """ @@ -1315,7 +1323,7 @@ def update_client( return self.put(f"/v2/api/clients/{client_id}", data={"client": body}) - def delete_client(self, client_id: UUIDLike) -> GlobusHTTPResponse: + def delete_client(self, client_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Delete a client. Requires the ``manage_projects`` scope. @@ -1344,7 +1352,7 @@ def delete_client(self, client_id: UUIDLike) -> GlobusHTTPResponse: """ return self.delete(f"/v2/api/clients/{client_id}") - def get_client_credentials(self, client_id: UUIDLike) -> IterableResponse: + def get_client_credentials(self, client_id: uuid.UUID | str) -> IterableResponse: """ Look up client credentials by ``client_id``. Requires the ``manage_projects`` scope. @@ -1387,7 +1395,7 @@ def get_client_credentials(self, client_id: UUIDLike) -> IterableResponse: def create_client_credential( self, - client_id: UUIDLike, + client_id: uuid.UUID | str, name: str, ) -> GlobusHTTPResponse: """ @@ -1436,8 +1444,8 @@ def create_client_credential( def delete_client_credential( self, - client_id: UUIDLike, - credential_id: UUIDLike, + client_id: uuid.UUID | str, + credential_id: uuid.UUID | str, ) -> GlobusHTTPResponse: """ Delete a credential. Requires the ``manage_projects`` scope. @@ -1469,7 +1477,7 @@ def delete_client_credential( """ return self.delete(f"/v2/api/clients/{client_id}/credentials/{credential_id}") - def get_scope(self, scope_id: UUIDLike) -> GlobusHTTPResponse: + def get_scope(self, scope_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Look up a scope by ``scope_id``. Requires the ``manage_projects`` scope. @@ -1515,7 +1523,7 @@ def get_scopes( self, *, scope_strings: t.Iterable[str] | str | MissingType = MISSING, - ids: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, + ids: t.Iterable[uuid.UUID | str] | uuid.UUID | str | MissingType = MISSING, query_params: dict[str, t.Any] | MissingType = MISSING, ) -> IterableResponse: """ @@ -1604,7 +1612,7 @@ def get_scopes( def create_scope( self, - client_id: UUIDLike, + client_id: uuid.UUID | str, name: str, description: str, scope_suffix: str, @@ -1673,7 +1681,7 @@ def create_scope( def update_scope( self, - scope_id: UUIDLike, + scope_id: uuid.UUID | str, *, name: str | MissingType = MISSING, description: str | MissingType = MISSING, @@ -1734,7 +1742,7 @@ def update_scope( return self.put(f"/v2/api/scopes/{scope_id}", data={"scope": body}) - def delete_scope(self, scope_id: UUIDLike) -> GlobusHTTPResponse: + def delete_scope(self, scope_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Delete a scope. Requires the ``manage_projects`` scope. @@ -1765,7 +1773,7 @@ def delete_scope(self, scope_id: UUIDLike) -> GlobusHTTPResponse: def get_consents( self, - identity_id: UUIDLike, + identity_id: uuid.UUID | str, *, # pylint: disable=redefined-builtin all: bool = False, diff --git a/src/globus_sdk/services/auth/data.py b/src/globus_sdk/services/auth/data.py index 76b9fac7e..4863cce71 100644 --- a/src/globus_sdk/services/auth/data.py +++ b/src/globus_sdk/services/auth/data.py @@ -1,4 +1,7 @@ -from globus_sdk._internal.type_definitions import UUIDLike +from __future__ import annotations + +import uuid + from globus_sdk._payload import GlobusPayload @@ -18,7 +21,7 @@ class DependentScopeSpec(GlobusPayload): def __init__( self, - scope: UUIDLike, + scope: uuid.UUID | str, optional: bool, requires_refresh_token: bool, ) -> None: diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index be470362f..a54ef28d9 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -2,10 +2,10 @@ import logging import typing as t +import uuid from globus_sdk import GlobusHTTPResponse, client from globus_sdk._internal.remarshal import strseq_listify -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.scopes import ComputeScopes @@ -73,7 +73,7 @@ def register_endpoint(self, data: dict[str, t.Any]) -> GlobusHTTPResponse: """ return self.post("/v2/endpoints", data=data) - def get_endpoint(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: + def get_endpoint(self, endpoint_id: uuid.UUID | str) -> GlobusHTTPResponse: """Get information about a registered endpoint. :param endpoint_id: The ID of the Globus Compute endpoint. @@ -88,7 +88,7 @@ def get_endpoint(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: """ # noqa: E501 return self.get(f"/v2/endpoints/{endpoint_id}") - def get_endpoint_status(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: + def get_endpoint_status(self, endpoint_id: uuid.UUID | str) -> GlobusHTTPResponse: """Get the status of a registered endpoint. :param endpoint_id: The ID of the Globus Compute endpoint. @@ -119,7 +119,7 @@ def get_endpoints(self, role: str | MissingType = MISSING) -> GlobusHTTPResponse query_params = {"role": role} return self.get("/v2/endpoints", query_params=query_params) - def delete_endpoint(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: + def delete_endpoint(self, endpoint_id: uuid.UUID | str) -> GlobusHTTPResponse: """Delete a registered endpoint. :param endpoint_id: The ID of the Globus Compute endpoint. @@ -134,7 +134,7 @@ def delete_endpoint(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: """ # noqa: E501 return self.delete(f"/v2/endpoints/{endpoint_id}") - def lock_endpoint(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: + def lock_endpoint(self, endpoint_id: uuid.UUID | str) -> GlobusHTTPResponse: """Temporarily block registration requests for the endpoint. :param endpoint_id: The ID of the Globus Compute endpoint. @@ -167,7 +167,7 @@ def register_function( """ # noqa: E501 return self.post("/v2/functions", data=function_data) - def get_function(self, function_id: UUIDLike) -> GlobusHTTPResponse: + def get_function(self, function_id: uuid.UUID | str) -> GlobusHTTPResponse: """Get information about a registered function. :param function_id: The ID of the function. @@ -182,7 +182,7 @@ def get_function(self, function_id: UUIDLike) -> GlobusHTTPResponse: """ # noqa: E501 return self.get(f"/v2/functions/{function_id}") - def delete_function(self, function_id: UUIDLike) -> GlobusHTTPResponse: + def delete_function(self, function_id: uuid.UUID | str) -> GlobusHTTPResponse: """Delete a registered function. :param function_id: The ID of the function. @@ -197,7 +197,7 @@ def delete_function(self, function_id: UUIDLike) -> GlobusHTTPResponse: """ # noqa: E501 return self.delete(f"/v2/functions/{function_id}") - def get_task(self, task_id: UUIDLike) -> GlobusHTTPResponse: + def get_task(self, task_id: uuid.UUID | str) -> GlobusHTTPResponse: """Get information about a task. :param task_id: The ID of the task. @@ -213,7 +213,7 @@ def get_task(self, task_id: UUIDLike) -> GlobusHTTPResponse: return self.get(f"/v2/tasks/{task_id}") def get_task_batch( - self, task_ids: UUIDLike | t.Iterable[UUIDLike] + self, task_ids: uuid.UUID | str | t.Iterable[uuid.UUID | str] ) -> GlobusHTTPResponse: """Get information about a batch of tasks. @@ -231,7 +231,7 @@ def get_task_batch( "/v2/batch_status", data={"task_ids": strseq_listify(task_ids)} ) - def get_task_group(self, task_group_id: UUIDLike) -> GlobusHTTPResponse: + def get_task_group(self, task_group_id: uuid.UUID | str) -> GlobusHTTPResponse: """Get a list of task IDs associated with a task group. :param task_group_id: The ID of the task group. @@ -290,7 +290,7 @@ def register_endpoint(self, data: dict[str, t.Any]) -> GlobusHTTPResponse: return self.post("/v3/endpoints", data=data) def update_endpoint( - self, endpoint_id: UUIDLike, data: dict[str, t.Any] + self, endpoint_id: uuid.UUID | str, data: dict[str, t.Any] ) -> GlobusHTTPResponse: """Update an endpoint. @@ -307,7 +307,7 @@ def update_endpoint( """ # noqa: E501 return self.put(f"/v3/endpoints/{endpoint_id}", data=data) - def lock_endpoint(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: + def lock_endpoint(self, endpoint_id: uuid.UUID | str) -> GlobusHTTPResponse: """Temporarily block registration requests for the endpoint. :param endpoint_id: The ID of the Globus Compute endpoint. @@ -322,7 +322,9 @@ def lock_endpoint(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: """ # noqa: E501 return self.post(f"/v3/endpoints/{endpoint_id}/lock") - def get_endpoint_allowlist(self, endpoint_id: UUIDLike) -> GlobusHTTPResponse: + def get_endpoint_allowlist( + self, endpoint_id: uuid.UUID | str + ) -> GlobusHTTPResponse: """Get a list of IDs for functions allowed to run on an endpoint. :param endpoint_id: The ID of the Globus Compute endpoint. @@ -353,7 +355,7 @@ def register_function(self, data: dict[str, t.Any]) -> GlobusHTTPResponse: return self.post("/v3/functions", data=data) def submit( - self, endpoint_id: UUIDLike, data: dict[str, t.Any] + self, endpoint_id: uuid.UUID | str, data: dict[str, t.Any] ) -> GlobusHTTPResponse: """Submit a batch of tasks to a Globus Compute endpoint. diff --git a/src/globus_sdk/services/compute/data.py b/src/globus_sdk/services/compute/data.py index ce9e9398b..4833a26df 100644 --- a/src/globus_sdk/services/compute/data.py +++ b/src/globus_sdk/services/compute/data.py @@ -1,6 +1,7 @@ from __future__ import annotations -from globus_sdk._internal.type_definitions import UUIDLike +import uuid + from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload from globus_sdk.exc import warn_deprecated @@ -53,7 +54,7 @@ def __init__( function_code: str, description: str | MissingType = MISSING, metadata: ComputeFunctionMetadata | MissingType = MISSING, - group: UUIDLike | MissingType = MISSING, + group: uuid.UUID | str | MissingType = MISSING, public: bool = False, ) -> None: warn_deprecated("ComputeFunctionDocument is deprecated.") diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 1318067e1..7cd746b72 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -14,7 +14,6 @@ ) from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.globus_app import GlobusApp @@ -69,7 +68,7 @@ def create_flow( run_managers: list[str] | MissingType = MISSING, run_monitors: list[str] | MissingType = MISSING, keywords: list[str] | MissingType = MISSING, - subscription_id: UUIDLike | None | MissingType = MISSING, + subscription_id: uuid.UUID | str | None | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ @@ -218,7 +217,7 @@ def create_flow( def get_flow( self, - flow_id: UUIDLike, + flow_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: @@ -372,7 +371,7 @@ def list_flows( def update_flow( self, - flow_id: UUIDLike, + flow_id: uuid.UUID | str, *, title: str | MissingType = MISSING, definition: dict[str, t.Any] | MissingType = MISSING, @@ -386,7 +385,7 @@ def update_flow( run_managers: list[str] | MissingType = MISSING, run_monitors: list[str] | MissingType = MISSING, keywords: list[str] | MissingType = MISSING, - subscription_id: UUIDLike | t.Literal["DEFAULT"] | MissingType = MISSING, + subscription_id: uuid.UUID | str | t.Literal["DEFAULT"] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: """ @@ -528,7 +527,7 @@ def update_flow( def delete_flow( self, - flow_id: UUIDLike, + flow_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> GlobusHTTPResponse: @@ -607,7 +606,9 @@ def validate_flow( def list_runs( self, *, - filter_flow_id: t.Iterable[UUIDLike] | UUIDLike | MissingType = MISSING, + filter_flow_id: ( + t.Iterable[uuid.UUID | str] | uuid.UUID | str | MissingType + ) = MISSING, filter_roles: str | t.Iterable[str] | MissingType = MISSING, marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -661,7 +662,7 @@ def list_runs( @paging.has_paginator(paging.MarkerPaginator, items_key="entries") def get_run_logs( self, - run_id: UUIDLike, + run_id: uuid.UUID | str, *, limit: int | MissingType = MISSING, reverse_order: bool | MissingType = MISSING, @@ -710,7 +711,7 @@ def get_run_logs( def get_run( self, - run_id: UUIDLike, + run_id: uuid.UUID | str, *, include_flow_description: bool | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -754,7 +755,7 @@ def get_run( def get_run_definition( self, - run_id: UUIDLike, + run_id: uuid.UUID | str, ) -> GlobusHTTPResponse: """ Get the flow definition and input schema at the time the run was started. @@ -785,7 +786,7 @@ def get_run_definition( return self.get(f"/runs/{run_id}/definition") - def cancel_run(self, run_id: UUIDLike) -> GlobusHTTPResponse: + def cancel_run(self, run_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Cancel a run. @@ -818,7 +819,7 @@ def cancel_run(self, run_id: UUIDLike) -> GlobusHTTPResponse: def update_run( self, - run_id: UUIDLike, + run_id: uuid.UUID | str, *, label: str | MissingType = MISSING, tags: list[str] | MissingType = MISSING, @@ -876,7 +877,7 @@ def update_run( } return self.put(f"/runs/{run_id}", data=data) - def delete_run(self, run_id: UUIDLike) -> GlobusHTTPResponse: + def delete_run(self, run_id: uuid.UUID | str) -> GlobusHTTPResponse: """ Delete a run. @@ -928,7 +929,7 @@ class SpecificFlowClient(client.BaseClient): def __init__( self, - flow_id: UUIDLike, + flow_id: uuid.UUID | str, *, environment: str | None = None, app: GlobusApp | None = None, @@ -953,7 +954,7 @@ def default_scope_requirements(self) -> list[Scope]: return [self.scopes.user] def add_app_transfer_data_access_scope( - self, collection_ids: UUIDLike | t.Iterable[UUIDLike] + self, collection_ids: uuid.UUID | str | t.Iterable[uuid.UUID | str] ) -> Self: """ Add a dependent ``data_access`` scope for one or more given ``collection_ids`` @@ -1061,7 +1062,7 @@ def run_flow( } return self.post(f"/flows/{self._flow_id}/run", data=data) - def resume_run(self, run_id: UUIDLike) -> GlobusHTTPResponse: + def resume_run(self, run_id: uuid.UUID | str) -> GlobusHTTPResponse: """ :param run_id: The ID of the run to resume diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 121bd50bd..922d7aefb 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -6,7 +6,6 @@ from globus_sdk import client, exc, paging, response from globus_sdk._internal.classprop import classproperty from globus_sdk._internal.remarshal import commajoin -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._internal.utils import slash_join from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import GlobusAuthorizer @@ -110,7 +109,7 @@ def get_gcs_collection_scopes( return GCSCollectionScopes(str(collection_id)) @staticmethod - def connector_id_to_name(connector_id: UUIDLike) -> str | None: + def connector_id_to_name(connector_id: uuid.UUID | str) -> str | None: """ .. warning:: @@ -279,7 +278,7 @@ def update_endpoint( def get_collection_list( self, *, - mapped_collection_id: UUIDLike | MissingType = MISSING, + mapped_collection_id: uuid.UUID | str | MissingType = MISSING, filter: ( # pylint: disable=redefined-builtin str | t.Iterable[str] | MissingType ) = MISSING, @@ -324,7 +323,7 @@ def get_collection_list( def get_collection( self, - collection_id: UUIDLike, + collection_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> UnpackingGCSResponse: @@ -382,7 +381,7 @@ def create_collection( def update_collection( self, - collection_id: UUIDLike, + collection_id: uuid.UUID | str, collection_data: dict[str, t.Any] | CollectionDocument, *, query_params: dict[str, t.Any] | None = None, @@ -415,7 +414,7 @@ def update_collection( def delete_collection( self, - collection_id: UUIDLike, + collection_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -519,7 +518,7 @@ def create_storage_gateway( def get_storage_gateway( self, - storage_gateway_id: UUIDLike, + storage_gateway_id: uuid.UUID | str, *, include: str | t.Iterable[str] | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -555,7 +554,7 @@ def get_storage_gateway( def update_storage_gateway( self, - storage_gateway_id: UUIDLike, + storage_gateway_id: uuid.UUID | str, data: dict[str, t.Any] | StorageGatewayDocument, *, query_params: dict[str, t.Any] | None = None, @@ -620,7 +619,7 @@ def delete_storage_gateway( ) def get_role_list( self, - collection_id: UUIDLike | MissingType = MISSING, + collection_id: uuid.UUID | str | MissingType = MISSING, include: str | MissingType = MISSING, page_size: int | MissingType = MISSING, marker: str | MissingType = MISSING, @@ -690,7 +689,7 @@ def create_role( def get_role( self, - role_id: UUIDLike, + role_id: uuid.UUID | str, query_params: dict[str, t.Any] | None = None, ) -> UnpackingGCSResponse: """ @@ -714,7 +713,7 @@ def get_role( def delete_role( self, - role_id: UUIDLike, + role_id: uuid.UUID | str, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ @@ -742,7 +741,7 @@ def delete_role( ) def get_user_credential_list( self, - storage_gateway: UUIDLike | MissingType = MISSING, + storage_gateway: uuid.UUID | str | MissingType = MISSING, page_size: int | MissingType = MISSING, marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -805,7 +804,7 @@ def create_user_credential( def get_user_credential( self, - user_credential_id: UUIDLike, + user_credential_id: uuid.UUID | str, query_params: dict[str, t.Any] | None = None, ) -> UnpackingGCSResponse: """ @@ -831,7 +830,7 @@ def get_user_credential( def update_user_credential( self, - user_credential_id: UUIDLike, + user_credential_id: uuid.UUID | str, data: dict[str, t.Any] | UserCredentialDocument, query_params: dict[str, t.Any] | None = None, ) -> UnpackingGCSResponse: @@ -860,7 +859,7 @@ def update_user_credential( def delete_user_credential( self, - user_credential_id: UUIDLike, + user_credential_id: uuid.UUID | str, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: """ diff --git a/src/globus_sdk/services/gcs/connector_table.py b/src/globus_sdk/services/gcs/connector_table.py index 3e8a66ad1..d147d97bf 100644 --- a/src/globus_sdk/services/gcs/connector_table.py +++ b/src/globus_sdk/services/gcs/connector_table.py @@ -3,8 +3,7 @@ import dataclasses import re import typing as t - -from globus_sdk._internal.type_definitions import UUIDLike +import uuid _NORMALIZATION_PATTERN = re.compile(r"[_\- ]+") @@ -96,7 +95,7 @@ def all_connectors(cls) -> t.Iterable[GlobusConnectServerConnector]: yield item @classmethod - def lookup(cls, name_or_id: UUIDLike) -> GlobusConnectServerConnector | None: + def lookup(cls, name_or_id: uuid.UUID | str) -> GlobusConnectServerConnector | None: """ Convert a name or ID into a connector object. Returns None if the name or ID is not recognized. @@ -121,7 +120,7 @@ def extend( cls, *, connector_name: str, - connector_id: UUIDLike, + connector_id: uuid.UUID | str, attribute_name: str | None = None, ) -> type[ConnectorTable]: """ diff --git a/src/globus_sdk/services/gcs/data/collection.py b/src/globus_sdk/services/gcs/data/collection.py index 99130a0be..3a993eac4 100644 --- a/src/globus_sdk/services/gcs/data/collection.py +++ b/src/globus_sdk/services/gcs/data/collection.py @@ -2,9 +2,9 @@ import abc import typing as t +import uuid from globus_sdk._internal.remarshal import strseq_listify -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload @@ -157,7 +157,7 @@ def __init__( department: str | None | MissingType = MISSING, description: str | None | MissingType = MISSING, display_name: str | MissingType = MISSING, - identity_id: UUIDLike | MissingType = MISSING, + identity_id: uuid.UUID | str | MissingType = MISSING, info_link: str | None | MissingType = MISSING, organization: str | MissingType = MISSING, restrict_transfers_to_high_assurance: ( @@ -277,7 +277,7 @@ def __init__( department: str | None | MissingType = MISSING, description: str | None | MissingType = MISSING, display_name: str | MissingType = MISSING, - identity_id: UUIDLike | MissingType = MISSING, + identity_id: uuid.UUID | str | MissingType = MISSING, info_link: str | None | MissingType = MISSING, organization: str | MissingType = MISSING, restrict_transfers_to_high_assurance: ( @@ -299,8 +299,8 @@ def __init__( # > specific args start < # strs domain_name: str | MissingType = MISSING, - guest_auth_policy_id: UUIDLike | None | MissingType = MISSING, - storage_gateway_id: UUIDLike | MissingType = MISSING, + guest_auth_policy_id: uuid.UUID | str | None | MissingType = MISSING, + storage_gateway_id: uuid.UUID | str | MissingType = MISSING, # str lists sharing_users_allow: t.Iterable[str] | None | MissingType = MISSING, sharing_users_deny: t.Iterable[str] | None | MissingType = MISSING, @@ -417,7 +417,7 @@ def __init__( department: str | None | MissingType = MISSING, description: str | None | MissingType = MISSING, display_name: str | MissingType = MISSING, - identity_id: UUIDLike | MissingType = MISSING, + identity_id: uuid.UUID | str | MissingType = MISSING, info_link: str | None | MissingType = MISSING, organization: str | MissingType = MISSING, restrict_transfers_to_high_assurance: ( @@ -439,8 +439,8 @@ def __init__( associated_flow_policy: dict[str, t.Any] | MissingType = MISSING, # > common args end < # > specific args start < - mapped_collection_id: UUIDLike | MissingType = MISSING, - user_credential_id: UUIDLike | MissingType = MISSING, + mapped_collection_id: uuid.UUID | str | MissingType = MISSING, + user_credential_id: uuid.UUID | str | MissingType = MISSING, skip_auto_delete: bool | MissingType = MISSING, activity_notification_policy: dict[str, list[str]] | MissingType = MISSING, # > specific args end < diff --git a/src/globus_sdk/services/gcs/data/role.py b/src/globus_sdk/services/gcs/data/role.py index 4dd6e726b..44e59b76b 100644 --- a/src/globus_sdk/services/gcs/data/role.py +++ b/src/globus_sdk/services/gcs/data/role.py @@ -1,8 +1,8 @@ from __future__ import annotations import typing as t +import uuid -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload @@ -25,7 +25,7 @@ class GCSRoleDocument(GlobusPayload): def __init__( self, DATA_TYPE: str = "role#1.0.0", - collection: UUIDLike | MissingType = MISSING, + collection: uuid.UUID | str | MissingType = MISSING, principal: str | MissingType = MISSING, role: str | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, diff --git a/src/globus_sdk/services/gcs/data/storage_gateway.py b/src/globus_sdk/services/gcs/data/storage_gateway.py index b373fafec..fd1203ef5 100644 --- a/src/globus_sdk/services/gcs/data/storage_gateway.py +++ b/src/globus_sdk/services/gcs/data/storage_gateway.py @@ -2,9 +2,9 @@ import copy import typing as t +import uuid from globus_sdk._internal.remarshal import list_map, listify, strseq_listify -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import AbstractGlobusPayload, GlobusPayload @@ -60,7 +60,7 @@ def __init__( self, DATA_TYPE: str | MissingType = MISSING, display_name: str | MissingType = MISSING, - connector_id: UUIDLike | MissingType = MISSING, + connector_id: uuid.UUID | str | MissingType = MISSING, root: str | MissingType = MISSING, identity_mappings: t.Iterable[dict[str, t.Any]] | MissingType = MISSING, policies: StorageGatewayPolicies | dict[str, t.Any] | MissingType = MISSING, diff --git a/src/globus_sdk/services/gcs/data/user_credential.py b/src/globus_sdk/services/gcs/data/user_credential.py index 4b228a7f7..fc467a518 100644 --- a/src/globus_sdk/services/gcs/data/user_credential.py +++ b/src/globus_sdk/services/gcs/data/user_credential.py @@ -1,8 +1,8 @@ from __future__ import annotations import typing as t +import uuid -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload @@ -28,11 +28,11 @@ class UserCredentialDocument(GlobusPayload): def __init__( self, DATA_TYPE: str = "user_credential#1.0.0", - identity_id: UUIDLike | MissingType = MISSING, - connector_id: UUIDLike | MissingType = MISSING, + identity_id: uuid.UUID | str | MissingType = MISSING, + connector_id: uuid.UUID | str | MissingType = MISSING, username: str | MissingType = MISSING, display_name: str | MissingType = MISSING, - storage_gateway_id: UUIDLike | MissingType = MISSING, + storage_gateway_id: uuid.UUID | str | MissingType = MISSING, policies: dict[str, t.Any] | MissingType = MISSING, additional_fields: dict[str, t.Any] | None = None, ) -> None: diff --git a/src/globus_sdk/services/groups/client.py b/src/globus_sdk/services/groups/client.py index 53aecf5c4..7759711ae 100644 --- a/src/globus_sdk/services/groups/client.py +++ b/src/globus_sdk/services/groups/client.py @@ -1,10 +1,10 @@ from __future__ import annotations import typing as t +import uuid from globus_sdk import client, response from globus_sdk._internal.remarshal import commajoin -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.scopes import GroupsScopes, Scope @@ -58,7 +58,7 @@ def get_my_groups( def get_group( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, *, include: str | t.Iterable[str] | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -86,7 +86,7 @@ def get_group( return self.get(f"/v2/groups/{group_id}", query_params=query_params) def get_group_by_subscription_id( - self, subscription_id: UUIDLike + self, subscription_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Using a subscription ID, find the group which provides that subscription. @@ -120,7 +120,7 @@ def get_group_by_subscription_id( def delete_group( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -168,7 +168,7 @@ def create_group( def update_group( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, data: dict[str, t.Any], *, query_params: dict[str, t.Any] | None = None, @@ -194,7 +194,7 @@ def update_group( def get_group_policies( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -218,7 +218,7 @@ def get_group_policies( def set_group_policies( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, data: dict[str, t.Any] | GroupPolicies, *, query_params: dict[str, t.Any] | None = None, @@ -299,7 +299,7 @@ def set_identity_preferences( def get_membership_fields( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -325,7 +325,7 @@ def get_membership_fields( def set_membership_fields( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, data: dict[t.Any, str], *, query_params: dict[str, t.Any] | None = None, @@ -355,7 +355,7 @@ def set_membership_fields( def batch_membership_action( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, actions: dict[str, t.Any] | BatchMembershipActions, *, query_params: dict[str, t.Any] | None = None, diff --git a/src/globus_sdk/services/groups/data.py b/src/globus_sdk/services/groups/data.py index 21605b096..b1bf6f289 100644 --- a/src/globus_sdk/services/groups/data.py +++ b/src/globus_sdk/services/groups/data.py @@ -2,9 +2,9 @@ import enum import typing as t +import uuid from globus_sdk._internal.remarshal import strseq_iter -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload @@ -107,7 +107,7 @@ class BatchMembershipActions(GlobusPayload): """ def accept_invites( - self, identity_ids: t.Iterable[UUIDLike] + self, identity_ids: t.Iterable[uuid.UUID | str] ) -> BatchMembershipActions: """ Accept invites for identities. The identities must belong to @@ -122,7 +122,7 @@ def accept_invites( def add_members( self, - identity_ids: t.Iterable[UUIDLike], + identity_ids: t.Iterable[uuid.UUID | str], *, role: _GROUP_ROLE_T = "member", ) -> BatchMembershipActions: @@ -139,7 +139,7 @@ def add_members( return self def approve_pending( - self, identity_ids: t.Iterable[UUIDLike] + self, identity_ids: t.Iterable[uuid.UUID | str] ) -> BatchMembershipActions: """ Approve a list of identities with pending join requests. @@ -152,7 +152,7 @@ def approve_pending( return self def decline_invites( - self, identity_ids: t.Iterable[UUIDLike] + self, identity_ids: t.Iterable[uuid.UUID | str] ) -> BatchMembershipActions: """ Decline an invitation for a given set of identities. @@ -166,7 +166,7 @@ def decline_invites( def invite_members( self, - identity_ids: t.Iterable[UUIDLike], + identity_ids: t.Iterable[uuid.UUID | str], *, role: _GROUP_ROLE_T = "member", ) -> BatchMembershipActions: @@ -182,7 +182,7 @@ def invite_members( ) return self - def join(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: + def join(self, identity_ids: t.Iterable[uuid.UUID | str]) -> BatchMembershipActions: """ Join a group with the given identities. The identities must be in the authenticated users identity set. @@ -194,7 +194,9 @@ def join(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: ) return self - def leave(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: + def leave( + self, identity_ids: t.Iterable[uuid.UUID | str] + ) -> BatchMembershipActions: """ Leave a group that one of the identities in the authenticated user's identity set is a member of. @@ -207,7 +209,7 @@ def leave(self, identity_ids: t.Iterable[UUIDLike]) -> BatchMembershipActions: return self def reject_join_requests( - self, identity_ids: t.Iterable[UUIDLike] + self, identity_ids: t.Iterable[uuid.UUID | str] ) -> BatchMembershipActions: """ Reject identities which have requested to join the group. @@ -220,7 +222,7 @@ def reject_join_requests( return self def remove_members( - self, identity_ids: t.Iterable[UUIDLike] + self, identity_ids: t.Iterable[uuid.UUID | str] ) -> BatchMembershipActions: """ Remove members from a group. This must be done as an admin or manager @@ -234,7 +236,7 @@ def remove_members( return self def request_join( - self, identity_ids: t.Iterable[UUIDLike] + self, identity_ids: t.Iterable[uuid.UUID | str] ) -> BatchMembershipActions: """ Request to join a group. diff --git a/src/globus_sdk/services/groups/manager.py b/src/globus_sdk/services/groups/manager.py index fa3ecf410..da9204f92 100644 --- a/src/globus_sdk/services/groups/manager.py +++ b/src/globus_sdk/services/groups/manager.py @@ -1,9 +1,9 @@ from __future__ import annotations import typing as t +import uuid from globus_sdk import response -from globus_sdk._internal.type_definitions import UUIDLike from .client import GroupsClient from .data import ( @@ -32,7 +32,7 @@ def create_group( name: str, description: str, *, - parent_id: UUIDLike | None = None, + parent_id: uuid.UUID | str | None = None, ) -> response.GlobusHTTPResponse: """ Create a group with the given name. If a parent ID is included, the @@ -51,7 +51,7 @@ def create_group( def set_group_policies( self, - group_id: UUIDLike, + group_id: uuid.UUID | str, *, is_high_assurance: bool, group_visibility: _GROUP_VISIBILITY_T, @@ -84,7 +84,7 @@ def set_group_policies( return self.client.set_group_policies(group_id, data=data) def accept_invite( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Accept invite for an identity. The identity must belong to @@ -98,8 +98,8 @@ def accept_invite( def add_member( self, - group_id: UUIDLike, - identity_id: UUIDLike, + group_id: uuid.UUID | str, + identity_id: uuid.UUID | str, *, role: _GROUP_ROLE_T = "member", ) -> response.GlobusHTTPResponse: @@ -114,7 +114,7 @@ def add_member( return self.client.batch_membership_action(group_id, actions) def approve_pending( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Approve an identity with a pending join request. @@ -126,7 +126,7 @@ def approve_pending( return self.client.batch_membership_action(group_id, actions) def decline_invite( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Decline an invitation for a given identity. @@ -139,8 +139,8 @@ def decline_invite( def invite_member( self, - group_id: UUIDLike, - identity_id: UUIDLike, + group_id: uuid.UUID | str, + identity_id: uuid.UUID | str, *, role: _GROUP_ROLE_T = "member", ) -> response.GlobusHTTPResponse: @@ -155,7 +155,7 @@ def invite_member( return self.client.batch_membership_action(group_id, actions) def join( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Join a group with the given identity. The identity must be in the @@ -168,7 +168,7 @@ def join( return self.client.batch_membership_action(group_id, actions) def leave( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Leave a group that one of the identities in the authenticated user's @@ -181,7 +181,7 @@ def leave( return self.client.batch_membership_action(group_id, actions) def reject_join_request( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Reject a member that has requested to join the group. @@ -193,7 +193,7 @@ def reject_join_request( return self.client.batch_membership_action(group_id, actions) def remove_member( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Remove a member from a group. This must be done as an admin or manager @@ -206,7 +206,7 @@ def remove_member( return self.client.batch_membership_action(group_id, actions) def request_join( - self, group_id: UUIDLike, identity_id: UUIDLike + self, group_id: uuid.UUID | str, identity_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: """ Request to join a group. diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index c29a426c3..23f41f801 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -2,10 +2,10 @@ import logging import typing as t +import uuid from globus_sdk import client, paging, response from globus_sdk._internal.remarshal import strseq_listify -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import SearchScopes @@ -80,7 +80,7 @@ def create_index( "/v1/index", data={"display_name": display_name, "description": description} ) - def delete_index(self, index_id: UUIDLike) -> response.GlobusHTTPResponse: + def delete_index(self, index_id: uuid.UUID | str) -> response.GlobusHTTPResponse: """ Mark an index for deletion. @@ -119,7 +119,7 @@ def delete_index(self, index_id: UUIDLike) -> response.GlobusHTTPResponse: log.debug(f"SearchClient.delete_index({index_id!r}, ...)") return self.delete(f"/v1/index/{index_id}") - def reopen_index(self, index_id: UUIDLike) -> response.GlobusHTTPResponse: + def reopen_index(self, index_id: uuid.UUID | str) -> response.GlobusHTTPResponse: """ Reopen an index that has been marked for deletion, cancelling the deletion. @@ -150,7 +150,7 @@ def reopen_index(self, index_id: UUIDLike) -> response.GlobusHTTPResponse: def get_index( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -230,7 +230,7 @@ def index_list( ) def search( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, q: str, *, offset: int | MissingType = MISSING, @@ -296,7 +296,7 @@ def search( ) def post_search( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, data: dict[str, t.Any] | SearchQuery, *, offset: int | MissingType = MISSING, @@ -365,7 +365,7 @@ def post_search( @paging.has_paginator(paging.MarkerPaginator, items_key="gmeta") def scroll( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, data: dict[str, t.Any] | SearchScrollQuery, *, marker: str | MissingType = MISSING, @@ -416,7 +416,7 @@ def scroll( # def ingest( - self, index_id: UUIDLike, data: dict[str, t.Any] + self, index_id: uuid.UUID | str, data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ Write data to a Search index as an asynchronous task. @@ -483,7 +483,7 @@ def ingest( # def delete_by_query( - self, index_id: UUIDLike, data: dict[str, t.Any] + self, index_id: uuid.UUID | str, data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ Delete data in a Search index as an asynchronous task, deleting all documents @@ -527,7 +527,7 @@ def delete_by_query( def batch_delete_by_subject( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, subjects: t.Iterable[str], additional_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -584,7 +584,7 @@ def batch_delete_by_subject( def get_subject( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, subject: str, *, query_params: dict[str, t.Any] | None = None, @@ -624,7 +624,7 @@ def get_subject( def delete_subject( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, subject: str, *, query_params: dict[str, t.Any] | None = None, @@ -672,7 +672,7 @@ def delete_subject( def get_entry( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, subject: str, *, entry_id: str | MissingType = MISSING, @@ -728,7 +728,7 @@ def get_entry( return self.get(f"/v1/index/{index_id}/entry", query_params=query_params) def create_entry( - self, index_id: UUIDLike, data: dict[str, t.Any] + self, index_id: uuid.UUID | str, data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ This API method is in effect an alias of ingest and is deprecated. @@ -792,7 +792,7 @@ def create_entry( return self.post(f"/v1/index/{index_id}/entry", data=data) def update_entry( - self, index_id: UUIDLike, data: dict[str, t.Any] + self, index_id: uuid.UUID | str, data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ This API method is in effect an alias of ingest and is deprecated. @@ -840,7 +840,7 @@ def update_entry( def delete_entry( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, subject: str, *, entry_id: str | MissingType = MISSING, @@ -901,7 +901,7 @@ def delete_entry( def get_task( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -934,7 +934,7 @@ def get_task( def get_task_list( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -972,7 +972,7 @@ def get_task_list( def create_role( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, data: dict[str, t.Any], *, query_params: dict[str, t.Any] | None = None, @@ -1016,7 +1016,7 @@ def create_role( def get_role_list( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -1041,7 +1041,7 @@ def get_role_list( def delete_role( self, - index_id: UUIDLike, + index_id: uuid.UUID | str, role_id: str, *, query_params: dict[str, t.Any] | None = None, diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index 9d153287f..aef77512f 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -6,7 +6,6 @@ from globus_sdk import client, exc, response from globus_sdk._internal import guards -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.scopes import ( GCSCollectionScopes, Scope, @@ -35,7 +34,7 @@ class TimersClient(client.BaseClient): default_scope_requirements = [TimersScopes.timer] def add_app_transfer_data_access_scope( - self, collection_ids: UUIDLike | t.Iterable[UUIDLike] + self, collection_ids: uuid.UUID | str | t.Iterable[uuid.UUID | str] ) -> TimersClient: """ Add a dependent ``data_access`` scope for one or more given ``collection_ids`` @@ -117,7 +116,7 @@ def list_jobs( def get_job( self, - job_id: UUIDLike, + job_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -208,7 +207,7 @@ def create_job( return self.post("/jobs/", data=data) def update_job( - self, job_id: UUIDLike, data: dict[str, t.Any] + self, job_id: uuid.UUID | str, data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ ``PATCH /jobs/`` @@ -226,7 +225,7 @@ def update_job( def delete_job( self, - job_id: UUIDLike, + job_id: uuid.UUID | str, ) -> response.GlobusHTTPResponse: """ ``DELETE /jobs/`` @@ -243,7 +242,7 @@ def delete_job( def pause_job( self, - job_id: UUIDLike, + job_id: uuid.UUID | str, ) -> response.GlobusHTTPResponse: """ Make a timer job inactive, preventing it from running until it is resumed. @@ -260,7 +259,7 @@ def pause_job( def resume_job( self, - job_id: UUIDLike, + job_id: uuid.UUID | str, *, update_credentials: bool | None = None, ) -> response.GlobusHTTPResponse: diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 9aba4ef0a..322b9dd42 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -8,7 +8,7 @@ from globus_sdk import client, exc, paging, response from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin -from globus_sdk._internal.type_definitions import DateLike, IntLike, UUIDLike +from globus_sdk._internal.type_definitions import DateLike, IntLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.scopes import GCSCollectionScopes, Scope, TransferScopes @@ -135,7 +135,7 @@ class TransferClient(client.BaseClient): default_scope_requirements = [TransferScopes.all] def add_app_data_access_scope( - self, collection_ids: UUIDLike | t.Iterable[UUIDLike] + self, collection_ids: uuid.UUID | str | t.Iterable[uuid.UUID | str] ) -> TransferClient: """ Add a dependent ``data_access`` scope for one or more given ``collection_ids`` @@ -219,7 +219,7 @@ def add_app_data_access_scope( def get_endpoint( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -250,7 +250,7 @@ def get_endpoint( def update_endpoint( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, data: dict[str, t.Any], *, query_params: dict[str, t.Any] | None = None, @@ -300,8 +300,8 @@ def update_endpoint( def set_subscription_id( self, - collection_id: UUIDLike, - subscription_id: UUIDLike | t.Literal["DEFAULT"] | None, + collection_id: uuid.UUID | str, + subscription_id: uuid.UUID | str | t.Literal["DEFAULT"] | None, ) -> response.GlobusHTTPResponse: """ Set the ``subscription_id`` on a mapped collection. @@ -359,7 +359,7 @@ def set_subscription_id( def set_subscription_admin_verified( self, - collection_id: UUIDLike, + collection_id: uuid.UUID | str, subscription_admin_verified: bool, ) -> response.GlobusHTTPResponse: """ @@ -430,7 +430,9 @@ def create_endpoint(self, data: dict[str, t.Any]) -> response.GlobusHTTPResponse log.debug("TransferClient.create_endpoint(...)") return self.post("/v0.10/endpoint", data=data) - def delete_endpoint(self, endpoint_id: UUIDLike) -> response.GlobusHTTPResponse: + def delete_endpoint( + self, endpoint_id: uuid.UUID | str + ) -> response.GlobusHTTPResponse: """ :param endpoint_id: ID of endpoint to delete @@ -466,7 +468,7 @@ def endpoint_search( *, filter_scope: str | MissingType = MISSING, filter_owner_id: str | MissingType = MISSING, - filter_host_endpoint: UUIDLike | MissingType = MISSING, + filter_host_endpoint: uuid.UUID | str | MissingType = MISSING, filter_non_functional: bool | MissingType = MISSING, filter_entity_type: ( t.Literal[ @@ -568,7 +570,7 @@ def endpoint_search( def endpoint_autoactivate( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, if_expires_in: int | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -597,7 +599,7 @@ def endpoint_autoactivate( def endpoint_deactivate( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -618,7 +620,7 @@ def endpoint_deactivate( def endpoint_activate( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, requirements_data: dict[str, t.Any] | None, query_params: dict[str, t.Any] | None = None, @@ -646,7 +648,7 @@ def endpoint_activate( def endpoint_get_activation_requirements( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> ActivationRequirementsResponse: @@ -670,7 +672,7 @@ def endpoint_get_activation_requirements( def my_effective_pause_rule_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: @@ -700,7 +702,7 @@ def my_effective_pause_rule_list( def my_shared_endpoint_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: @@ -731,7 +733,7 @@ def my_shared_endpoint_list( @paging.has_paginator(paging.NextTokenPaginator, items_key="shared_endpoints") def get_shared_endpoint_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, max_results: int | MissingType = MISSING, next_token: str | MissingType = MISSING, @@ -813,7 +815,7 @@ def create_shared_endpoint( def endpoint_server_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: @@ -840,7 +842,7 @@ def endpoint_server_list( def get_endpoint_server( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, server_id: IntLike, *, query_params: dict[str, t.Any] | None = None, @@ -868,7 +870,7 @@ def get_endpoint_server( ) def add_endpoint_server( - self, endpoint_id: UUIDLike, server_data: dict[str, t.Any] + self, endpoint_id: uuid.UUID | str, server_data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ .. warning:: @@ -884,7 +886,7 @@ def add_endpoint_server( def update_endpoint_server( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, server_id: IntLike, server_data: dict[str, t.Any], ) -> response.GlobusHTTPResponse: @@ -908,7 +910,7 @@ def update_endpoint_server( ) def delete_endpoint_server( - self, endpoint_id: UUIDLike, server_id: IntLike + self, endpoint_id: uuid.UUID | str, server_id: IntLike ) -> response.GlobusHTTPResponse: """ .. warning:: @@ -930,7 +932,7 @@ def delete_endpoint_server( def endpoint_role_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: @@ -956,7 +958,7 @@ def endpoint_role_list( ) def add_endpoint_role( - self, endpoint_id: UUIDLike, role_data: dict[str, t.Any] + self, endpoint_id: uuid.UUID | str, role_data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ :param endpoint_id: The endpoint on which the role is being added @@ -976,7 +978,7 @@ def add_endpoint_role( def get_endpoint_role( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, role_id: str, *, query_params: dict[str, t.Any] | None = None, @@ -1001,7 +1003,7 @@ def get_endpoint_role( ) def delete_endpoint_role( - self, endpoint_id: UUIDLike, role_id: str + self, endpoint_id: uuid.UUID | str, role_id: str ) -> response.GlobusHTTPResponse: """ :param endpoint_id: The endpoint on which the role applies @@ -1025,7 +1027,7 @@ def delete_endpoint_role( def endpoint_acl_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: @@ -1051,7 +1053,7 @@ def endpoint_acl_list( def get_endpoint_acl_rule( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, rule_id: str, *, query_params: dict[str, t.Any] | None = None, @@ -1078,7 +1080,7 @@ def get_endpoint_acl_rule( ) def add_endpoint_acl_rule( - self, endpoint_id: UUIDLike, rule_data: dict[str, t.Any] + self, endpoint_id: uuid.UUID | str, rule_data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ :param endpoint_id: ID of endpoint to which to add the acl @@ -1116,7 +1118,7 @@ def add_endpoint_acl_rule( def update_endpoint_acl_rule( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, rule_id: str, rule_data: dict[str, t.Any], ) -> response.GlobusHTTPResponse: @@ -1144,7 +1146,7 @@ def update_endpoint_acl_rule( ) def delete_endpoint_acl_rule( - self, endpoint_id: UUIDLike, rule_id: str + self, endpoint_id: uuid.UUID | str, rule_id: str ) -> response.GlobusHTTPResponse: """ :param endpoint_id: The endpoint on which the access rule applies @@ -1208,7 +1210,7 @@ def create_bookmark( def get_bookmark( self, - bookmark_id: UUIDLike, + bookmark_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -1229,7 +1231,7 @@ def get_bookmark( return self.get(f"/v0.10/bookmark/{bookmark_id}", query_params=query_params) def update_bookmark( - self, bookmark_id: UUIDLike, bookmark_data: dict[str, t.Any] + self, bookmark_id: uuid.UUID | str, bookmark_data: dict[str, t.Any] ) -> response.GlobusHTTPResponse: """ :param bookmark_id: The ID of the bookmark to modify @@ -1247,7 +1249,9 @@ def update_bookmark( log.debug(f"TransferClient.update_bookmark({bookmark_id})") return self.put(f"/v0.10/bookmark/{bookmark_id}", data=bookmark_data) - def delete_bookmark(self, bookmark_id: UUIDLike) -> response.GlobusHTTPResponse: + def delete_bookmark( + self, bookmark_id: uuid.UUID | str + ) -> response.GlobusHTTPResponse: """ :param bookmark_id: The ID of the bookmark to delete @@ -1269,7 +1273,7 @@ def delete_bookmark(self, bookmark_id: UUIDLike) -> response.GlobusHTTPResponse: def operation_ls( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, path: str | MissingType = MISSING, *, show_hidden: bool | MissingType = MISSING, @@ -1379,7 +1383,7 @@ def operation_ls( def operation_mkdir( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, path: str, *, local_user: str | MissingType = MISSING, @@ -1427,7 +1431,7 @@ def operation_mkdir( def operation_rename( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, oldpath: str, newpath: str, *, @@ -1478,7 +1482,7 @@ def operation_rename( def operation_stat( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, path: str | MissingType = MISSING, *, local_user: str | MissingType = MISSING, @@ -1524,7 +1528,7 @@ def operation_stat( def operation_symlink( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, symlink_target: str, path: str, *, @@ -1801,7 +1805,7 @@ def task_list( ) def task_event_list( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, limit: int | MissingType = MISSING, offset: int | MissingType = MISSING, @@ -1851,7 +1855,7 @@ def task_event_list( def get_task( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -1873,7 +1877,7 @@ def get_task( def update_task( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, data: dict[str, t.Any], *, query_params: dict[str, t.Any] | None = None, @@ -1898,7 +1902,7 @@ def update_task( log.debug(f"TransferClient.update_task({task_id}, ...)") return self.put(f"/v0.10/task/{task_id}", data=data, query_params=query_params) - def cancel_task(self, task_id: UUIDLike) -> response.GlobusHTTPResponse: + def cancel_task(self, task_id: uuid.UUID | str) -> response.GlobusHTTPResponse: """ Cancel a task which is still running. @@ -1917,7 +1921,7 @@ def cancel_task(self, task_id: UUIDLike) -> response.GlobusHTTPResponse: return self.post(f"/v0.10/task/{task_id}/cancel") def task_wait( - self, task_id: UUIDLike, *, timeout: int = 10, polling_interval: int = 10 + self, task_id: uuid.UUID | str, *, timeout: int = 10, polling_interval: int = 10 ) -> bool: r""" Wait until a Task is complete or fails, with a time limit. If the task @@ -2021,7 +2025,7 @@ def timed_out(waited_time: int) -> bool: def task_pause_info( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -2048,7 +2052,7 @@ def task_pause_info( ) def task_successful_transfers( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -2107,7 +2111,7 @@ def task_successful_transfers( ) def task_skipped_errors( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -2185,7 +2189,7 @@ def endpoint_manager_monitored_endpoints( def endpoint_manager_hosted_endpoint_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: @@ -2216,7 +2220,7 @@ def endpoint_manager_hosted_endpoint_list( def endpoint_manager_get_endpoint( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -2242,7 +2246,7 @@ def endpoint_manager_get_endpoint( def endpoint_manager_acl_list( self, - endpoint_id: UUIDLike, + endpoint_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: @@ -2280,9 +2284,11 @@ def endpoint_manager_task_list( self, *, filter_status: str | t.Iterable[str] | MissingType = MISSING, - filter_task_id: UUIDLike | t.Iterable[UUIDLike] | MissingType = MISSING, - filter_owner_id: UUIDLike | MissingType = MISSING, - filter_endpoint: UUIDLike | MissingType = MISSING, + filter_task_id: ( + uuid.UUID | str | t.Iterable[uuid.UUID | str] | MissingType + ) = MISSING, + filter_owner_id: uuid.UUID | str | MissingType = MISSING, + filter_endpoint: uuid.UUID | str | MissingType = MISSING, filter_endpoint_use: t.Literal["source", "destination"] | MissingType = MISSING, filter_is_paused: bool | MissingType = MISSING, filter_completion_time: str | tuple[DateLike, DateLike] | MissingType = MISSING, @@ -2420,7 +2426,7 @@ def endpoint_manager_task_list( def endpoint_manager_get_task( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -2454,7 +2460,7 @@ def endpoint_manager_get_task( ) def endpoint_manager_task_event_list( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, limit: int | MissingType = MISSING, offset: int | MissingType = MISSING, @@ -2507,7 +2513,7 @@ def endpoint_manager_task_event_list( def endpoint_manager_task_pause_info( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -2538,7 +2544,7 @@ def endpoint_manager_task_pause_info( ) def endpoint_manager_task_successful_transfers( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -2584,7 +2590,7 @@ def endpoint_manager_task_successful_transfers( ) def endpoint_manager_task_skipped_errors( self, - task_id: UUIDLike, + task_id: uuid.UUID | str, *, marker: str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, @@ -2626,7 +2632,7 @@ def endpoint_manager_task_skipped_errors( def endpoint_manager_cancel_tasks( self, - task_ids: t.Iterable[UUIDLike], + task_ids: t.Iterable[uuid.UUID | str], message: str, *, query_params: dict[str, t.Any] | None = None, @@ -2659,7 +2665,7 @@ def endpoint_manager_cancel_tasks( def endpoint_manager_cancel_status( self, - admin_cancel_id: UUIDLike, + admin_cancel_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -2687,7 +2693,7 @@ def endpoint_manager_cancel_status( def endpoint_manager_pause_tasks( self, - task_ids: t.Iterable[UUIDLike], + task_ids: t.Iterable[uuid.UUID | str], message: str, *, query_params: dict[str, t.Any] | None = None, @@ -2720,7 +2726,7 @@ def endpoint_manager_pause_tasks( def endpoint_manager_resume_tasks( self, - task_ids: t.Iterable[UUIDLike], + task_ids: t.Iterable[uuid.UUID | str], *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -2754,7 +2760,7 @@ def endpoint_manager_resume_tasks( def endpoint_manager_pause_rule_list( self, *, - filter_endpoint: UUIDLike | MissingType = MISSING, + filter_endpoint: uuid.UUID | str | MissingType = MISSING, query_params: dict[str, t.Any] | None = None, ) -> IterableTransferResponse: """ @@ -2824,7 +2830,7 @@ def endpoint_manager_create_pause_rule( def endpoint_manager_get_pause_rule( self, - pause_rule_id: UUIDLike, + pause_rule_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: @@ -2852,7 +2858,7 @@ def endpoint_manager_get_pause_rule( def endpoint_manager_update_pause_rule( self, - pause_rule_id: UUIDLike, + pause_rule_id: uuid.UUID | str, data: dict[str, t.Any] | None, ) -> response.GlobusHTTPResponse: """ @@ -2891,7 +2897,7 @@ def endpoint_manager_update_pause_rule( def endpoint_manager_delete_pause_rule( self, - pause_rule_id: UUIDLike, + pause_rule_id: uuid.UUID | str, *, query_params: dict[str, t.Any] | None = None, ) -> response.GlobusHTTPResponse: diff --git a/src/globus_sdk/services/transfer/data/delete_data.py b/src/globus_sdk/services/transfer/data/delete_data.py index ca5fbdedf..8cd28efc0 100644 --- a/src/globus_sdk/services/transfer/data/delete_data.py +++ b/src/globus_sdk/services/transfer/data/delete_data.py @@ -3,9 +3,9 @@ import datetime import logging import typing as t +import uuid from globus_sdk._internal.remarshal import stringify -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload @@ -77,10 +77,10 @@ class DeleteData(GlobusPayload): def __init__( self, - endpoint: UUIDLike, + endpoint: uuid.UUID | str, *, label: str | MissingType = MISSING, - submission_id: UUIDLike | MissingType = MISSING, + submission_id: uuid.UUID | str | MissingType = MISSING, recursive: bool | MissingType = MISSING, ignore_missing: bool | MissingType = MISSING, interpret_globs: bool | MissingType = MISSING, diff --git a/src/globus_sdk/services/transfer/data/transfer_data.py b/src/globus_sdk/services/transfer/data/transfer_data.py index 55dc99313..30b6c27f6 100644 --- a/src/globus_sdk/services/transfer/data/transfer_data.py +++ b/src/globus_sdk/services/transfer/data/transfer_data.py @@ -3,8 +3,8 @@ import datetime import logging import typing as t +import uuid -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload @@ -154,11 +154,11 @@ class TransferData(GlobusPayload): def __init__( self, - source_endpoint: UUIDLike, - destination_endpoint: UUIDLike, + source_endpoint: uuid.UUID | str, + destination_endpoint: uuid.UUID | str, *, label: str | MissingType = MISSING, - submission_id: UUIDLike | MissingType = MISSING, + submission_id: uuid.UUID | str | MissingType = MISSING, sync_level: ( int | t.Literal["exists", "size", "mtime", "checksum"] | MissingType ) = MISSING, diff --git a/src/globus_sdk/token_storage/v2/base.py b/src/globus_sdk/token_storage/v2/base.py index abee7d166..66056ff4a 100644 --- a/src/globus_sdk/token_storage/v2/base.py +++ b/src/globus_sdk/token_storage/v2/base.py @@ -7,9 +7,9 @@ import re import sys import typing as t +import uuid import globus_sdk -from globus_sdk._internal.type_definitions import UUIDLike from .token_data import TokenStorageData @@ -168,7 +168,7 @@ def __init_subclass__(cls, **kwargs: t.Any): @classmethod def for_globus_app( cls, - client_id: UUIDLike, + client_id: uuid.UUID | str, app_name: str, config: GlobusAppConfig, namespace: str, @@ -219,7 +219,7 @@ def user_only_umask(self) -> t.Iterator[None]: def _default_globus_app_filepath( - client_id: UUIDLike, app_name: str, environment: str + client_id: uuid.UUID | str, app_name: str, environment: str ) -> str: r""" Construct a default TokenStorage filepath for a GlobusApp. diff --git a/src/globus_sdk/token_storage/v2/memory.py b/src/globus_sdk/token_storage/v2/memory.py index 30b02bed5..8705d9b8f 100644 --- a/src/globus_sdk/token_storage/v2/memory.py +++ b/src/globus_sdk/token_storage/v2/memory.py @@ -1,12 +1,12 @@ from __future__ import annotations import typing as t +import uuid from .base import TokenStorage from .token_data import TokenStorageData if t.TYPE_CHECKING: - from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.globus_app import GlobusAppConfig @@ -28,7 +28,7 @@ def __init__(self, *, namespace: str = "DEFAULT") -> None: def for_globus_app( cls, # pylint: disable=unused-argument - client_id: UUIDLike, + client_id: uuid.UUID | str, app_name: str, config: GlobusAppConfig, # pylint: enable=unused-argument diff --git a/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py b/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py index fbd2349cb..27e559da3 100644 --- a/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py +++ b/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py @@ -1,9 +1,9 @@ from __future__ import annotations +import uuid from datetime import datetime from globus_sdk import GlobusError, Scope -from globus_sdk._internal.type_definitions import UUIDLike class TokenValidationError(GlobusError): @@ -24,7 +24,9 @@ class MissingIdentityError(IdentityValidationError, LookupError): class IdentityMismatchError(IdentityValidationError, ValueError): """The identity in a token response did not match the expected identity.""" - def __init__(self, message: str, stored_id: UUIDLike, new_id: UUIDLike) -> None: + def __init__( + self, message: str, stored_id: uuid.UUID | str, new_id: uuid.UUID | str + ) -> None: super().__init__(message) self.stored_id = stored_id self.new_id = new_id diff --git a/tests/common/consents.py b/tests/common/consents.py index 62f8bf3c3..c1e7920c2 100644 --- a/tests/common/consents.py +++ b/tests/common/consents.py @@ -5,7 +5,6 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk.scopes import Scope, ScopeParser from globus_sdk.scopes.consents import Consent, ConsentForest @@ -21,11 +20,11 @@ class ConsentTest(Consent): Required fields: client, scope, scope_name """ - client: UUIDLike - scope: UUIDLike + client: uuid.UUID | str + scope: uuid.UUID | str scope_name: str id: int = field(default_factory=lambda: uuid.uuid1().int) - effective_identity: UUIDLike = str(uuid.uuid4()) + effective_identity: uuid.UUID | str = str(uuid.uuid4()) dependency_path: list[int] = field(default_factory=list) created: datetime = field( default_factory=lambda: datetime.now() - timedelta(days=1) diff --git a/tests/unit/helpers/gcs/test_collections.py b/tests/unit/helpers/gcs/test_collections.py index 6970988b7..9a3f77f06 100644 --- a/tests/unit/helpers/gcs/test_collections.py +++ b/tests/unit/helpers/gcs/test_collections.py @@ -12,7 +12,6 @@ POSIXCollectionPolicies, POSIXStagingCollectionPolicies, ) -from globus_sdk._internal.type_definitions import UUIDLike from globus_sdk._missing import MISSING, MissingType, filter_missing from globus_sdk.transport import JSONRequestEncoder @@ -253,7 +252,7 @@ def test_mapped_collection_opt_bool(fieldname, value): ("department", (str, None, MissingType)), ("description", (str, None, MissingType)), ("display_name", (str, MissingType)), - ("identity_id", (UUIDLike, MissingType)), + ("identity_id", (t.Union[uuid.UUID, str], MissingType)), ("info_link", (str, None, MissingType)), ("organization", (str, MissingType)), ("user_message", (str, None, MissingType)), @@ -270,7 +269,7 @@ def test_mapped_collection_opt_bool(fieldname, value): mapped_collection_fields = [ *common_collection_fields, ("domain_name", (str, MissingType)), - ("guest_auth_policy_id", (UUIDLike, None, MissingType)), + ("guest_auth_policy_id", (t.Union[uuid.UUID, str], None, MissingType)), ("disable_anonymous_writes", (bool, MissingType)), ("policies", (t.Dict[str, t.Any], MissingType)), ] @@ -278,8 +277,8 @@ def test_mapped_collection_opt_bool(fieldname, value): guest_collection_fields = [ *common_collection_fields, - ("mapped_collection_id", (UUIDLike, MissingType)), - ("user_credential_id", (UUIDLike, MissingType)), + ("mapped_collection_id", (t.Union[uuid.UUID, str], MissingType)), + ("user_credential_id", (t.Union[uuid.UUID, str], MissingType)), ("activity_notification_policy", (t.Dict[str, t.List[str]], MissingType)), ] @@ -308,7 +307,7 @@ def _gen_value(_type): return ["STRING"] if _type is bool: return [True, False] - if _type is UUIDLike: + if _type is t.Union[uuid.UUID, str]: return [str(uuid.uuid1()), uuid.uuid1()] if _type is t.Iterable[str]: return [[], ["a", "b", "c"]] From 9e476cde17d04d89eb5951d5319eb2a41d487e5b Mon Sep 17 00:00:00 2001 From: m1yag1 <8730430+m1yag1@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:42:12 -0500 Subject: [PATCH 100/176] Improve upgrading guide for optional scope dependencies --- docs/upgrading.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 1a3ba6586..88232b9fb 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -179,6 +179,24 @@ Update ``add_dependency`` usage like so: my_scope = Scope(ROOT_SCOPE_STRING) my_scope = my_scope.with_dependency(DEPENCENCY_STRING) +For optional dependencies, the ``optional`` parameter must now be specified when +creating the dependency scope, not when adding it: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.scopes import Scope + + my_scope = Scope(ROOT_SCOPE_STRING) + my_scope.add_dependency(DEPENDENCY_STRING, optional=True) + + # globus-sdk v4 + from globus_sdk.scopes import Scope + + my_scope = Scope(ROOT_SCOPE_STRING) + dependency = Scope(DEPENDENCY_STRING, optional=True) + my_scope = my_scope.with_dependency(dependency) + ScopeParser Is Now Separate from Scope ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -198,7 +216,7 @@ For example, update like so: my_scopes: list[Scope] = Scope.parse(scope_string) # globus-sdk v4 - from globus_sdk.scopes import Scope, Scopeparser + from globus_sdk.scopes import Scope, ScopeParser my_scopes: list[Scope] = ScopeParser.parse(scope_string) From eca28da471d5671d236c63c2e6989d43da65aa4f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 17 Jul 2025 11:36:16 -0500 Subject: [PATCH 101/176] Remove the experimental GlobusApp aliases These are deprecated and marked for removal. --- docs/upgrading.rst | 1 + src/globus_sdk/experimental/globus_app.py | 46 ------------------- .../unit/experimental/test_legacy_support.py | 18 -------- 3 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 src/globus_sdk/experimental/globus_app.py diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 1a3ba6586..aaa869087 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -238,6 +238,7 @@ The removed alias and new module names are shown in the table below. :header: "Removed alias", "New name" "``globus_sdk.experimental.auth_requirements_error``", "``globus_sdk.gare``" + "``globus_sdk.experimental.globus_app``", "``globus_sdk.globus_app``" "``globus_sdk.experimental.scope_parser``", "``globus_sdk.scopes``" "``globus_sdk.experimental.consents``", "``globus_sdk.scopes.consents``" "``globus_sdk.experimental.tokenstorage``", "``globus_sdk.token_storage``" diff --git a/src/globus_sdk/experimental/globus_app.py b/src/globus_sdk/experimental/globus_app.py deleted file mode 100644 index edaa3d61c..000000000 --- a/src/globus_sdk/experimental/globus_app.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -import sys -import typing as t - -__all__ = ( - "GlobusApp", - "UserApp", - "ClientApp", - "GlobusAppConfig", - "TokenValidationErrorHandler", -) - -# legacy aliases -# (when accessed, these will emit deprecation warnings) -if t.TYPE_CHECKING: - from globus_sdk.globus_app import ( - ClientApp, - GlobusApp, - GlobusAppConfig, - TokenValidationErrorHandler, - UserApp, - ) -else: - - def __getattr__(name: str) -> t.Any: - import globus_sdk.globus_app as globus_app_module - from globus_sdk.exc import warn_deprecated - - # Certain GlobusApp constructs are exposed from `globus_sdk`. - new_import_path = f"globus_sdk.globus_app.{name}" - if name in ("GlobusApp", "UserApp", "ClientApp", "GlobusAppConfig"): - new_import_path = f"globus_sdk.{name}" - - warn_deprecated( - "'globus_sdk.experimental.globus_app' has been renamed to " - "'globus_sdk.globus_app'. " - f"Importing '{name}' from `globus_sdk.experimental` is deprecated. " - f"Use `{new_import_path}` instead." - ) - - value = getattr(globus_app_module, name, None) - if value is None: - raise AttributeError(f"module {__name__} has no attribute {name}") - setattr(sys.modules[__name__], name, value) - return value diff --git a/tests/unit/experimental/test_legacy_support.py b/tests/unit/experimental/test_legacy_support.py index 349f8c1a1..f5c0a74f2 100644 --- a/tests/unit/experimental/test_legacy_support.py +++ b/tests/unit/experimental/test_legacy_support.py @@ -21,21 +21,3 @@ def test_login_flow_manager_importable_from_experimental(): LocalServerLoginFlowManager, LoginFlowManager, ) - - -def test_globus_app_importable_from_experimental(): - # This construct should be imported from `globus_sdk.globus_app`. - with pytest.warns(RemovedInV4Warning, match=r"globus_sdk\.globus_app\."): - from globus_sdk.experimental.globus_app import ( # noqa: F401 - TokenValidationErrorHandler, - ) - - # Each of these constructs should be imported from `globus_sdk`. - # This regex ensures we didn't include `globus_sdk.globus_app.` in any warning - with pytest.warns(RemovedInV4Warning, match=r"^(?!globus_sdk\.globus_app\.).*$"): - from globus_sdk.experimental.globus_app import ( # noqa: F401 - ClientApp, - GlobusApp, - GlobusAppConfig, - UserApp, - ) From 28b422c5551fbb0a54eb050a454cd569ba9dada2 Mon Sep 17 00:00:00 2001 From: Mike Arbelaez <8730430+m1yag1@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:57:05 -0500 Subject: [PATCH 102/176] Add RequestCallerInfo data object to RequestsTransport.request (#1261) This replaces the authorizer parameter in RequestsTransport.request() with a required caller_info parameter containing `RequestCallerInfo` data object. The change generalizes request caller context information, making the API more explicit. * Add `RequestCallerInfo` data object to `globus_sdk.transport.requests` * Replace authorizer parameter with required `caller_info` in `RequestsTransport.request()` * Update RetryContext to require non-nullable `caller_info` * Create RequestCallerInfo objects instead of passing authorizer directly --- ...09_8730430+m1yag1_sc_43037_caller_info.rst | 4 ++ src/globus_sdk/client.py | 11 ++-- src/globus_sdk/transport/__init__.py | 3 +- src/globus_sdk/transport/requests.py | 28 +++++++--- src/globus_sdk/transport/retry.py | 11 ++-- .../base_client/test_retry_behavior.py | 35 +++++++++++++ .../transport/test_default_retry_policy.py | 29 +++++++++-- .../unit/transport/test_retry_check_runner.py | 14 +++-- .../unit/transport/test_transfer_transport.py | 11 ++-- .../test_transport_authz_handling.py | 51 ++++++++++++++++++- 10 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst diff --git a/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst b/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst new file mode 100644 index 000000000..8364d2a95 --- /dev/null +++ b/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst @@ -0,0 +1,4 @@ +Added +----- + +- Add ``RequestCallerInfo`` data object to ``RequestsTransport.request`` for passing caller context information. (:pr:`NUMBER`) diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 90d4a192b..9f441d6c7 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -12,7 +12,7 @@ from globus_sdk.paging import PaginatorTable from globus_sdk.response import GlobusHTTPResponse from globus_sdk.scopes import Scope, ScopeCollection -from globus_sdk.transport import RequestsTransport +from globus_sdk.transport import RequestCallerInfo, RequestsTransport if sys.version_info >= (3, 10): from typing import TypeAlias @@ -496,16 +496,19 @@ def request( else: authorizer = None + # create caller info with the authorizer + caller_info = RequestCallerInfo(authorizer=authorizer) + # make the request log.debug("request will hit URL: %s", url) r = self.transport.request( - method=method, - url=url, + method, + url, + caller_info=caller_info, data=data, query_params=query_params, headers=rheaders, encoding=encoding, - authorizer=authorizer, allow_redirects=allow_redirects, stream=stream, ) diff --git a/src/globus_sdk/transport/__init__.py b/src/globus_sdk/transport/__init__.py index 6d2c77e6a..3fdc2fb47 100644 --- a/src/globus_sdk/transport/__init__.py +++ b/src/globus_sdk/transport/__init__.py @@ -1,6 +1,6 @@ from ._clientinfo import GlobusClientInfo from .encoders import FormRequestEncoder, JSONRequestEncoder, RequestEncoder -from .requests import RequestsTransport +from .requests import RequestCallerInfo, RequestsTransport from .retry import ( RetryCheck, RetryCheckFlags, @@ -12,6 +12,7 @@ __all__ = ( "RequestsTransport", + "RequestCallerInfo", "RetryCheck", "RetryCheckFlags", "RetryCheckResult", diff --git a/src/globus_sdk/transport/requests.py b/src/globus_sdk/transport/requests.py index 0531e42f5..649f060fb 100644 --- a/src/globus_sdk/transport/requests.py +++ b/src/globus_sdk/transport/requests.py @@ -30,6 +30,17 @@ log = logging.getLogger(__name__) +class RequestCallerInfo: + """ + Data object that holds contextual information about the caller of a request. + + :param authorizer: The authorizer object from the client making the request + """ + + def __init__(self, *, authorizer: GlobusAuthorizer | None = None) -> None: + self.authorizer = authorizer + + def _parse_retry_after(response: requests.Response) -> int | None: val = response.headers.get("Retry-After") if not val: @@ -302,11 +313,12 @@ def request( self, method: str, url: str, + *, + caller_info: RequestCallerInfo, query_params: dict[str, t.Any] | None = None, data: dict[str, t.Any] | list[t.Any] | str | bytes | None = None, headers: dict[str, str] | None = None, encoding: str | None = None, - authorizer: GlobusAuthorizer | None = None, allow_redirects: bool = True, stream: bool = False, ) -> requests.Response: @@ -315,6 +327,8 @@ def request( :param url: URL for the request :param method: HTTP request method, as an all caps string + :param caller_info: Contextual information about the caller of the request, + including the authorizer. :param query_params: Parameters to be encoded as a query string :param headers: HTTP headers to add to the request :param data: Data to send as the request body. May pass through encoding. @@ -322,8 +336,6 @@ def request( are all valid values. Custom encodings can be used only if they are registered with the transport. By default, strings get "text" behavior and all other objects get "json". - :param authorizer: The authorizer which is used to get or update authorization - information for the request :param allow_redirects: Follow Location headers on redirect response automatically. Defaults to ``True`` :param stream: Do not immediately download the response content. Defaults to @@ -335,15 +347,16 @@ def request( resp: requests.Response | None = None req = self._encode(method, url, query_params, data, headers, encoding) checker = RetryCheckRunner(self.retry_checks) + log.debug("transport request state initialized") for attempt in range(self.max_retries + 1): log.debug("transport request retry cycle. attempt=%d", attempt) # add Authorization header, or (if it's a NullAuthorizer) possibly # explicitly remove the Authorization header # done fresh for each request, to handle potential for refreshed credentials - self._set_authz_header(authorizer, req) + self._set_authz_header(caller_info.authorizer, req) - ctx = RetryContext(attempt, authorizer=authorizer) + ctx = RetryContext(attempt, caller_info=caller_info) try: log.debug("request about to send") resp = ctx.response = self.session.send( @@ -468,13 +481,14 @@ def default_check_expired_authorization( """ if ( # is the current check applicable? ctx.response is None - or ctx.authorizer is None + or ctx.caller_info is None + or ctx.caller_info.authorizer is None or ctx.response.status_code not in self.EXPIRED_AUTHORIZATION_STATUS_CODES ): return RetryCheckResult.no_decision # run the authorizer's handler, and 'do_retry' if the handler indicated # that it was able to make a change which should make the request retryable - if ctx.authorizer.handle_missing_authorization(): + if ctx.caller_info.authorizer.handle_missing_authorization(): return RetryCheckResult.do_retry return RetryCheckResult.no_decision diff --git a/src/globus_sdk/transport/retry.py b/src/globus_sdk/transport/retry.py index fc2a39523..54202017a 100644 --- a/src/globus_sdk/transport/retry.py +++ b/src/globus_sdk/transport/retry.py @@ -6,7 +6,8 @@ import requests -from globus_sdk.authorizers import GlobusAuthorizer +if t.TYPE_CHECKING: + from globus_sdk.transport.requests import RequestCallerInfo log = logging.getLogger(__name__) @@ -24,23 +25,23 @@ class RetryContext: or ``exception`` will be present. :param attempt: The request attempt number, starting at 0. + :param caller_info: Contextual information about the caller, including authorizer :param response: The response on a successful request :param exception: The error raised when trying to send the request - :param authorizer: The authorizer object from the client making the request """ def __init__( self, attempt: int, *, - authorizer: GlobusAuthorizer | None = None, + caller_info: RequestCallerInfo, response: requests.Response | None = None, exception: Exception | None = None, ) -> None: # retry attempt number self.attempt = attempt - # if there is an authorizer for the request, it will be available in the context - self.authorizer = authorizer + # caller info provides contextual information about the request + self.caller_info = caller_info # the response or exception from a request # we expect exactly one of these to be non-null self.response = response diff --git a/tests/functional/base_client/test_retry_behavior.py b/tests/functional/base_client/test_retry_behavior.py index edabc272d..fa5a83ea4 100644 --- a/tests/functional/base_client/test_retry_behavior.py +++ b/tests/functional/base_client/test_retry_behavior.py @@ -3,6 +3,7 @@ import globus_sdk from globus_sdk.testing import RegisteredResponse, load_response +from globus_sdk.transport import RequestCallerInfo @pytest.mark.parametrize("error_status", [500, 429, 502, 503, 504]) @@ -268,3 +269,37 @@ def handle_missing_authorization(self): # and that between the two calls, handle_missing_authorization was called once # but the handler should not be called a second time because the 401 repeated assert dummy_authz_calls == ["set_authz", "handle_missing", "set_authz"] + + +def test_transport_caller_info_with_retry(client): + load_response( + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" + ) + ) + load_response( + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) + ) + + dummy_authz_calls = [] + + class DummyAuthorizer(globus_sdk.authorizers.GlobusAuthorizer): + def get_authorization_header(self): + dummy_authz_calls.append("set_authz") + return "foo" + + def handle_missing_authorization(self): + dummy_authz_calls.append("handle_missing") + return True + + authorizer = DummyAuthorizer() + caller_info = RequestCallerInfo(authorizer=authorizer) + + # Test direct transport usage with caller_info + response = client.transport.request( + "GET", "https://foo.api.globus.org/bar", caller_info=caller_info + ) + + assert response.status_code == 200 + # Verify that the authorizer was used for both authorization and retry handling + assert dummy_authz_calls == ["set_authz", "handle_missing", "set_authz"] diff --git a/tests/unit/transport/test_default_retry_policy.py b/tests/unit/transport/test_default_retry_policy.py index a774fd23c..9c13e296b 100644 --- a/tests/unit/transport/test_default_retry_policy.py +++ b/tests/unit/transport/test_default_retry_policy.py @@ -3,6 +3,7 @@ import pytest from globus_sdk.transport import ( + RequestCallerInfo, RequestsTransport, RetryCheckResult, RetryCheckRunner, @@ -18,7 +19,8 @@ def test_retry_policy_respects_retry_after(mocksleep, http_status): dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "5"} dummy_response.status_code = http_status - ctx = RetryContext(1, response=dummy_response) + caller_info = RequestCallerInfo(authorizer=None) + ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True mocksleep.assert_not_called() @@ -35,7 +37,8 @@ def test_retry_policy_ignores_retry_after_too_high(mocksleep, http_status): dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "20"} dummy_response.status_code = http_status - ctx = RetryContext(1, response=dummy_response) + caller_info = RequestCallerInfo(authorizer=None) + ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True mocksleep.assert_not_called() @@ -51,7 +54,8 @@ def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "not-an-integer"} dummy_response.status_code = http_status - ctx = RetryContext(1, response=dummy_response) + caller_info = RequestCallerInfo(authorizer=None) + ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True mocksleep.assert_not_called() @@ -69,5 +73,22 @@ def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): def test_default_retry_check_noop_on_exception(checkname, mocksleep): transport = RequestsTransport() method = getattr(transport, checkname) - ctx = RetryContext(1, exception=Exception("foo")) + caller_info = RequestCallerInfo(authorizer=None) + ctx = RetryContext(1, caller_info=caller_info, exception=Exception("foo")) assert method(ctx) is RetryCheckResult.no_decision + + +def test_retry_context_accepts_caller_info(): + mock_authorizer = mock.Mock() + caller_info = RequestCallerInfo(authorizer=mock_authorizer) + + ctx = RetryContext(1, caller_info=caller_info) + + assert ctx.caller_info is caller_info + assert ctx.caller_info.authorizer is mock_authorizer + + +def test_retry_context_caller_info_none(): + ctx = RetryContext(1, caller_info=None) + + assert ctx.caller_info is None diff --git a/tests/unit/transport/test_retry_check_runner.py b/tests/unit/transport/test_retry_check_runner.py index 7a0e759b7..9d095ccf0 100644 --- a/tests/unit/transport/test_retry_check_runner.py +++ b/tests/unit/transport/test_retry_check_runner.py @@ -1,17 +1,23 @@ from unittest import mock -from globus_sdk.transport import RetryCheckResult, RetryCheckRunner, RetryContext +from globus_sdk.transport import ( + RequestCallerInfo, + RetryCheckResult, + RetryCheckRunner, + RetryContext, +) def _make_test_retry_context(*, status=200, exception=None, response=None): + caller_info = RequestCallerInfo(authorizer=None) if exception: - return RetryContext(1, exception=exception) + return RetryContext(1, caller_info=caller_info, exception=exception) elif response: - return RetryContext(1, response=response) + return RetryContext(1, caller_info=caller_info, response=response) dummy_response = mock.Mock() dummy_response.status_code = 200 - return RetryContext(1, response=dummy_response) + return RetryContext(1, caller_info=caller_info, response=dummy_response) def test_retry_check_runner_should_retry_explicit_on_first_check(): diff --git a/tests/unit/transport/test_transfer_transport.py b/tests/unit/transport/test_transfer_transport.py index f806b6d9f..7fb93c1ff 100644 --- a/tests/unit/transport/test_transfer_transport.py +++ b/tests/unit/transport/test_transfer_transport.py @@ -1,7 +1,7 @@ from unittest import mock from globus_sdk.services.transfer.transport import TransferRequestsTransport -from globus_sdk.transport import RetryCheckRunner, RetryContext +from globus_sdk.transport import RequestCallerInfo, RetryCheckRunner, RetryContext def test_transfer_does_not_retry_external(): @@ -19,7 +19,8 @@ def test_transfer_does_not_retry_external(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - ctx = RetryContext(1, response=dummy_response) + caller_info = RequestCallerInfo(authorizer=None) + ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False @@ -42,7 +43,8 @@ def test_transfer_does_not_retry_endpoint_error(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - ctx = RetryContext(1, response=dummy_response) + caller_info = RequestCallerInfo(authorizer=None) + ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False @@ -57,6 +59,7 @@ def _raise_value_error(): dummy_response = mock.Mock() dummy_response.json = _raise_value_error dummy_response.status_code = 502 - ctx = RetryContext(1, response=dummy_response) + caller_info = RequestCallerInfo(authorizer=None) + ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True diff --git a/tests/unit/transport/test_transport_authz_handling.py b/tests/unit/transport/test_transport_authz_handling.py index 0f4eb2251..bfa23f599 100644 --- a/tests/unit/transport/test_transport_authz_handling.py +++ b/tests/unit/transport/test_transport_authz_handling.py @@ -1,7 +1,9 @@ from unittest import mock +import pytest + from globus_sdk.authorizers import NullAuthorizer -from globus_sdk.transport import RequestsTransport +from globus_sdk.transport import RequestCallerInfo, RequestsTransport def test_will_not_modify_authz_header_without_authorizer(): @@ -28,3 +30,50 @@ def test_will_null_authz_header_with_null_authorizer(): request.headers["Authorization"] = "foo bar" transport._set_authz_header(NullAuthorizer(), request) assert request.headers == {} + + +def test_request_caller_info_creation(): + mock_authorizer = mock.Mock() + caller_info = RequestCallerInfo(authorizer=mock_authorizer) + + assert caller_info.authorizer is mock_authorizer + + +def test_requests_transport_accepts_caller_info(): + transport = RequestsTransport() + mock_authorizer = mock.Mock() + mock_authorizer.get_authorization_header.return_value = "Bearer token" + caller_info = RequestCallerInfo(authorizer=mock_authorizer) + + with mock.patch.object(transport, "session") as mock_session: + mock_response = mock.Mock(status_code=200) + mock_session.send.return_value = mock_response + + response = transport.request( + "GET", "https://example.com", caller_info=caller_info + ) + + assert response.status_code == 200 + + sent_request = mock_session.send.call_args[0][0] + assert sent_request.headers["Authorization"] == "Bearer token" + + +def test_requests_transport_caller_info_required(): + transport = RequestsTransport() + + with pytest.raises(TypeError): + transport.request("GET", "https://example.com") + + +def test_requests_transport_keyword_only(): + transport = RequestsTransport() + caller_info = RequestCallerInfo(authorizer=None) + + with pytest.raises(TypeError): + transport.request("GET", "https://example.com", caller_info) + + +def test_request_caller_info_with_none_authorizer(): + caller_info = RequestCallerInfo(authorizer=None) + assert caller_info.authorizer is None From f082a2ca17e314a931f4b2b13ddd42665ed0d57f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 17 Jul 2025 20:57:13 +0000 Subject: [PATCH 103/176] (actions) update PR references --- .../20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst b/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst index 8364d2a95..34d0df5f7 100644 --- a/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst +++ b/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst @@ -1,4 +1,4 @@ Added ----- -- Add ``RequestCallerInfo`` data object to ``RequestsTransport.request`` for passing caller context information. (:pr:`NUMBER`) +- Add ``RequestCallerInfo`` data object to ``RequestsTransport.request`` for passing caller context information. From 211306b1d8a761f23206a9a6a4745ee039ca4b94 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 17 Jul 2025 16:29:24 -0500 Subject: [PATCH 104/176] Fix the PR autofixer job to use fetch-depth=0 This fixer recently made a bad edit because it didn't observe the repo correctly. The changelog fragment is also fixed here. Attempting to reproduce the bad behavior, it seemed only to be possible based on a shallow clone of the repo. --- .github/workflows/update_pr_references.yaml | 2 ++ .../20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update_pr_references.yaml b/.github/workflows/update_pr_references.yaml index 0dbd053ce..35ec35ab7 100644 --- a/.github/workflows/update_pr_references.yaml +++ b/.github/workflows/update_pr_references.yaml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.x' diff --git a/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst b/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst index 34d0df5f7..ba29f1210 100644 --- a/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst +++ b/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst @@ -1,4 +1,4 @@ Added ----- -- Add ``RequestCallerInfo`` data object to ``RequestsTransport.request`` for passing caller context information. +- Add ``RequestCallerInfo`` data object to ``RequestsTransport.request`` for passing caller context information. (:pr:`1261`) From 3aed16c526db227a6fa74e286ca0474586a1ddf5 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 18 Jul 2025 15:48:21 -0500 Subject: [PATCH 105/176] Remove the experimental login_flows alias Part of the planned changes for SDK v4. --- docs/upgrading.rst | 1 + .../experimental/login_flow_manager.py | 37 ------------------- .../unit/experimental/test_legacy_support.py | 23 ------------ 3 files changed, 1 insertion(+), 60 deletions(-) delete mode 100644 src/globus_sdk/experimental/login_flow_manager.py delete mode 100644 tests/unit/experimental/test_legacy_support.py diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 1c9526831..7d9c97f9f 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -260,6 +260,7 @@ The removed alias and new module names are shown in the table below. "``globus_sdk.experimental.scope_parser``", "``globus_sdk.scopes``" "``globus_sdk.experimental.consents``", "``globus_sdk.scopes.consents``" "``globus_sdk.experimental.tokenstorage``", "``globus_sdk.token_storage``" + "``globus_sdk.experimental.login_flow_manager``", "``globus_sdk.login_flows``" ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/globus_sdk/experimental/login_flow_manager.py b/src/globus_sdk/experimental/login_flow_manager.py deleted file mode 100644 index c4edd67a4..000000000 --- a/src/globus_sdk/experimental/login_flow_manager.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import sys -import typing as t - -__all__ = ( - "CommandLineLoginFlowManager", - "LocalServerLoginFlowManager", - "LoginFlowManager", -) - -# legacy aliases -# (when accessed, these will emit deprecation warnings) -if t.TYPE_CHECKING: - from globus_sdk.login_flows import ( - CommandLineLoginFlowManager, - LocalServerLoginFlowManager, - LoginFlowManager, - ) -else: - - def __getattr__(name: str) -> t.Any: - import globus_sdk.login_flows as login_flows_module - from globus_sdk.exc import warn_deprecated - - warn_deprecated( - "'globus_sdk.experimental.login_flow_manager' has been renamed to " - "'globus_sdk.login_flows'. " - f"Importing '{name}' from `globus_sdk.experimental` is deprecated. " - f"Use `globus_sdk.login_flows.{name}` instead." - ) - - value = getattr(login_flows_module, name, None) - if value is None: - raise AttributeError(f"module {__name__} has no attribute {name}") - setattr(sys.modules[__name__], name, value) - return value diff --git a/tests/unit/experimental/test_legacy_support.py b/tests/unit/experimental/test_legacy_support.py deleted file mode 100644 index f5c0a74f2..000000000 --- a/tests/unit/experimental/test_legacy_support.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Constructs which are added to `experimental` ultimately (hopefully) get ported over to - the main `globus_sdk` namespace. - -The tests in this module verify that those constructs are still available from the - `globus_sdk.experimental` namespace (for backwards compatibility). - -Eventually these constructs do get deprecated at which point the tests in this module - can be deleted. -""" - -import pytest - -from globus_sdk import RemovedInV4Warning - - -def test_login_flow_manager_importable_from_experimental(): - with pytest.warns(RemovedInV4Warning): - from globus_sdk.experimental.login_flow_manager import ( # noqa: F401 - CommandLineLoginFlowManager, - LocalServerLoginFlowManager, - LoginFlowManager, - ) From 9cad5f9711e8a88034a9eb51bc902157735d7bc9 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 18 Jul 2025 16:26:14 -0500 Subject: [PATCH 106/176] Remove deprecated 'oauth2_validate_token' method --- ...8_162517_sirosen_remove_validate_token.rst | 6 ++++ .../services/auth/client/base_login_client.py | 36 ------------------- .../auth/base/test_oauth2_validate_token.py | 18 ---------- 3 files changed, 6 insertions(+), 54 deletions(-) create mode 100644 changelog.d/20250718_162517_sirosen_remove_validate_token.rst delete mode 100644 tests/functional/services/auth/base/test_oauth2_validate_token.py diff --git a/changelog.d/20250718_162517_sirosen_remove_validate_token.rst b/changelog.d/20250718_162517_sirosen_remove_validate_token.rst new file mode 100644 index 000000000..06b03d592 --- /dev/null +++ b/changelog.d/20250718_162517_sirosen_remove_validate_token.rst @@ -0,0 +1,6 @@ +Breaking Changes +---------------- + +- The ``oauth2_validate_token`` method has been removed from + ``NativeAppAuthClient`` and ``ConfidentialAppAuthClient``. + This method was deprecated in globus-sdk v3. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 06b2d5973..ab86cd661 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -7,7 +7,6 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from globus_sdk import client, exc -from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import GlobusAuthorizer, NullAuthorizer @@ -257,41 +256,6 @@ def oauth2_refresh_token( form_data, body_params=body_params, response_class=OAuthRefreshTokenResponse ) - def oauth2_validate_token( - self, - token: str, - *, - body_params: dict[str, t.Any] | None = None, - ) -> GlobusHTTPResponse: - """ - Deprecated. Because the validity of a token may be dependent on policies - enforced both by Globus Auth and the resource server, this method is not - considered a reliable way to check token validity. - Users are encouraged to treat tokens as valid until proven otherwise instead. - - :param token: The token which should be validated. Can be a refresh token or an - access token - :param body_params: Additional parameters to include in the validation - body. Primarily for internal use - """ - exc.warn_deprecated( - f"{self.__class__.__name__}.oauth2_validate_token() is deprecated. " - "This validation method gives non-definitive results. " - "Tokens should be treated as valid until they are used and their " - "validity can be assessed." - ) - log.debug("Validating token") - body = {"token": token} - - # if this client has no way of authenticating itself but - # it does have a client_id, we'll send that in the request - no_authentication = guards.is_optional(self.authorizer, NullAuthorizer) - if no_authentication and self.client_id: - log.debug("Validating token with unauthenticated client") - body.update({"client_id": self.client_id}) - body.update(body_params or {}) - return self.post("/v2/oauth2/token/validate", data=body, encoding="form") - def oauth2_revoke_token( self, token: str, diff --git a/tests/functional/services/auth/base/test_oauth2_validate_token.py b/tests/functional/services/auth/base/test_oauth2_validate_token.py deleted file mode 100644 index af766fbeb..000000000 --- a/tests/functional/services/auth/base/test_oauth2_validate_token.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -import globus_sdk -from globus_sdk.testing import RegisteredResponse, load_response - - -def test_oauth2_validate_token_emits_deprecation_warning(): - nc = globus_sdk.NativeAppAuthClient("dummy_client_id") - load_response( - RegisteredResponse( - service="auth", - path="/v2/oauth2/token/validate", - method="POST", - json={"foo": "bar"}, - ) - ) - with pytest.warns(globus_sdk.RemovedInV4Warning): - nc.oauth2_validate_token("dummy_token") From 1aafe1b71e0237b8a5c7c9c1c43eb5b21fb781f7 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 18 Jul 2025 21:38:58 +0000 Subject: [PATCH 107/176] (actions) update PR references --- changelog.d/20250718_162517_sirosen_remove_validate_token.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250718_162517_sirosen_remove_validate_token.rst b/changelog.d/20250718_162517_sirosen_remove_validate_token.rst index 06b03d592..d354cab95 100644 --- a/changelog.d/20250718_162517_sirosen_remove_validate_token.rst +++ b/changelog.d/20250718_162517_sirosen_remove_validate_token.rst @@ -3,4 +3,4 @@ Breaking Changes - The ``oauth2_validate_token`` method has been removed from ``NativeAppAuthClient`` and ``ConfidentialAppAuthClient``. - This method was deprecated in globus-sdk v3. (:pr:`NUMBER`) + This method was deprecated in globus-sdk v3. (:pr:`1270`) From d7101e3811c1865dd0ae6d9981d38a06d0267d6f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 18 Jul 2025 15:52:02 -0500 Subject: [PATCH 108/176] Rename 'function_data' to 'data' This was desired shortly after the first release containing `function_data`, but we held the change back at the time to wait for SDK v4 to make a technically breaking change. See also: #1092 --- .../20250718_155001_sirosen_rename_function_data.rst | 5 +++++ src/globus_sdk/services/compute/client.py | 9 +++------ .../services/compute/v2/test_register_function.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 changelog.d/20250718_155001_sirosen_rename_function_data.rst diff --git a/changelog.d/20250718_155001_sirosen_rename_function_data.rst b/changelog.d/20250718_155001_sirosen_rename_function_data.rst new file mode 100644 index 000000000..775e95e64 --- /dev/null +++ b/changelog.d/20250718_155001_sirosen_rename_function_data.rst @@ -0,0 +1,5 @@ +Breaking Changes +---------------- + +- The ``function_data`` argument to ``ComputeClientV2.register_function`` has + been renamed to ``data`` to be consistent with other usages. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index a54ef28d9..2287f07b6 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -149,13 +149,10 @@ def lock_endpoint(self, endpoint_id: uuid.UUID | str) -> GlobusHTTPResponse: """ # noqa: E501 return self.post(f"/v2/endpoints/{endpoint_id}/lock") - def register_function( - self, - function_data: dict[str, t.Any], - ) -> GlobusHTTPResponse: + def register_function(self, data: dict[str, t.Any]) -> GlobusHTTPResponse: """Register a new function. - :param function_data: A function registration document. + :param data: A function registration document. .. tab-set:: @@ -165,7 +162,7 @@ def register_function( :service: compute :ref: Functions/operation/register_function_v2_functions_post """ # noqa: E501 - return self.post("/v2/functions", data=function_data) + return self.post("/v2/functions", data=data) def get_function(self, function_id: uuid.UUID | str) -> GlobusHTTPResponse: """Get information about a registered function. diff --git a/tests/functional/services/compute/v2/test_register_function.py b/tests/functional/services/compute/v2/test_register_function.py index 7a93bd598..55390bf6a 100644 --- a/tests/functional/services/compute/v2/test_register_function.py +++ b/tests/functional/services/compute/v2/test_register_function.py @@ -8,6 +8,6 @@ def test_register_function(compute_client_v2: globus_sdk.ComputeClientV2): "function_name": meta["function_name"], "function_code": meta["function_code"], } - res = compute_client_v2.register_function(function_data=registration_doc) + res = compute_client_v2.register_function(data=registration_doc) assert res.http_status == 200 assert res.data["function_uuid"] == meta["function_id"] From 7843ec5011dec5b92ef2f4cd5b5395b61b30aafd Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 18 Jul 2025 17:54:58 -0500 Subject: [PATCH 109/176] Remove 'TimerJob.from_transfer_data' constructor (#1269) Co-authored-by: Kurt McKee --- ...5657_sirosen_remove_from_transfer_data.rst | 6 ++ src/globus_sdk/services/timers/client.py | 8 ++- src/globus_sdk/services/timers/data.py | 66 ------------------- tests/functional/services/timers/test_jobs.py | 32 ++++----- tests/unit/helpers/test_timer.py | 24 ------- 5 files changed, 24 insertions(+), 112 deletions(-) create mode 100644 changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst diff --git a/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst b/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst new file mode 100644 index 000000000..af4444738 --- /dev/null +++ b/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst @@ -0,0 +1,6 @@ +Breaking Changes +---------------- + +- The ``TimerJob.from_transfer_data`` classmethod, which was deprecated in + globus-sdk version 3, has been removed. Users should use the ``TransferTimer`` + class to construct timers which submit transfer tasks. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index aef77512f..6f98def1e 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -188,10 +188,12 @@ def create_job( **Examples** >>> from datetime import datetime, timedelta - >>> transfer_data = TransferData(...) + >>> callback_url = ... + >>> data = ... >>> timers_client = globus_sdk.TimersClient(...) - >>> job = TimerJob.from_transfer_data( - ... transfer_data, + >>> job = TimerJob( + ... callback_url, + ... data, ... datetime.utcnow(), ... timedelta(days=14), ... name="my-timer-job" diff --git a/src/globus_sdk/services/timers/data.py b/src/globus_sdk/services/timers/data.py index 18a9f6b4c..a051f5f03 100644 --- a/src/globus_sdk/services/timers/data.py +++ b/src/globus_sdk/services/timers/data.py @@ -6,11 +6,8 @@ import logging import typing as t -from globus_sdk._internal.utils import slash_join from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -from globus_sdk.config import get_service_url -from globus_sdk.exc import warn_deprecated from globus_sdk.services.transfer import TransferData log = logging.getLogger(__name__) @@ -266,69 +263,6 @@ def __init__( if scope is not None: self["scope"] = scope - @classmethod - def from_transfer_data( - cls, - transfer_data: TransferData | dict[str, t.Any], - start: dt.datetime | str, - interval: dt.timedelta | int | None, - *, - name: str | None = None, - stop_after: dt.datetime | None = None, - stop_after_n: int | None = None, - scope: str | None = None, - environment: str | None = None, - ) -> TimerJob: - r""" - Specify data to create a Timers job using the parameters for a transfer. Timers - will use those parameters to run the defined transfer operation, recurring at - the given interval. - - :param transfer_data: A :class:`TransferData ` object. - Construct this object exactly as you would normally; Timers will use this to - run the recurring transfer. - :param start: The datetime at which to start the Timers job. - :param interval: The interval at which the Timers job should recur. Interpreted - as seconds if specified as an integer. If ``stop_after_n == 1``, i.e. the - job is set to run only a single time, then interval *must* be None. - :param name: A (not necessarily unique) name to identify this job in Timers - :param stop_after: A date after which the Timers job will stop running - :param stop_after_n: A number of executions after which the Timers job will stop - :param scope: Timers defaults to the Transfer 'all' scope. Use this parameter to - change the scope used by Timers when calling the Transfer Action Provider. - :param environment: For internal use: because this method needs to generate a - URL for the Transfer Action Provider, this argument can control which - environment the Timers job is sent to. - """ - warn_deprecated( - "TimerJob.from_transfer_data(X, ...) is deprecated. " - "Prefer TransferTimer(body=X, ...) instead." - ) - - transfer_action_url = slash_join( - get_service_url("actions", environment=environment), "transfer/transfer/run" - ) - log.debug( - "Creating TimerJob from TransferData, action_url=%s", transfer_action_url - ) - for key in ("submission_id", "skip_activation_check"): - if transfer_data.get(key, MISSING) is not MISSING: - raise ValueError( - f"cannot create TimerJob from TransferData which has {key} set" - ) - # dict will either convert a `TransferData` object or leave us with a dict here - callback_body = {"body": dict(transfer_data)} - return cls( - transfer_action_url, - callback_body, - start, - interval, - name=name, - stop_after=stop_after, - stop_after_n=stop_after_n, - scope=scope, - ) - def _format_date(date: str | dt.datetime | MissingType) -> str | MissingType: if isinstance(date, dt.datetime): diff --git a/tests/functional/services/timers/test_jobs.py b/tests/functional/services/timers/test_jobs.py index b2d18b989..a69e37c9a 100644 --- a/tests/functional/services/timers/test_jobs.py +++ b/tests/functional/services/timers/test_jobs.py @@ -3,10 +3,8 @@ import pytest -from globus_sdk import TimerJob, TimersAPIError, TransferData, config, exc -from globus_sdk._internal.utils import slash_join +from globus_sdk import TimerJob, TimersAPIError from globus_sdk.testing import get_last_request, load_response -from tests.common import GO_EP1_ID, GO_EP2_ID def test_list_jobs(client): @@ -39,17 +37,14 @@ def test_get_job_errors(client): ) def test_create_job(client, start, interval): meta = load_response(client.create_job).metadata - transfer_data = TransferData(GO_EP1_ID, GO_EP2_ID) - with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): - timer_job = TimerJob.from_transfer_data(transfer_data, start, interval) - response = client.create_job(timer_job) - assert response.http_status == 201 - assert response.data["job_id"] == meta["job_id"] - with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): - timer_job = TimerJob.from_transfer_data(dict(transfer_data), start, interval) + timer_job = TimerJob( + "https://example.bogus/bogus-callback", {"bogus": "bogus_body"}, start, interval + ) + response = client.create_job(timer_job) assert response.http_status == 201 assert response.data["job_id"] == meta["job_id"] + req_body = json.loads(get_last_request().body) if isinstance(start, datetime.datetime): assert req_body["start"] == start.isoformat() @@ -59,18 +54,17 @@ def test_create_job(client, start, interval): assert req_body["interval"] == interval.total_seconds() else: assert req_body["interval"] == interval - assert req_body["callback_url"] == slash_join( - config.get_service_url("actions"), "/transfer/transfer/run" - ) + assert req_body["callback_url"] == "https://example.bogus/bogus-callback" def test_create_job_validation_error(client): meta = load_response(client.create_job, case="validation_error").metadata - transfer_data = TransferData(GO_EP1_ID, GO_EP2_ID) - with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): - timer_job = TimerJob.from_transfer_data( - transfer_data, "2022-04-05T06:00:00", 1800 - ) + timer_job = TimerJob( + "https://example.bogus/bogus-callback", + {"bogus": "bogus_body"}, + "2022-04-05T06:00:00", + 1800, + ) with pytest.raises(TimersAPIError) as excinfo: client.create_job(timer_job) diff --git a/tests/unit/helpers/test_timer.py b/tests/unit/helpers/test_timer.py index c66a0498f..7439df560 100644 --- a/tests/unit/helpers/test_timer.py +++ b/tests/unit/helpers/test_timer.py @@ -5,37 +5,13 @@ from globus_sdk import ( OnceTimerSchedule, RecurringTimerSchedule, - TimerJob, TransferData, TransferTimer, - exc, ) from globus_sdk._missing import filter_missing from tests.common import GO_EP1_ID, GO_EP2_ID -def test_timer_from_transfer_data_ok(): - tdata = TransferData(GO_EP1_ID, GO_EP2_ID) - with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): - job = TimerJob.from_transfer_data(tdata, "2022-01-01T00:00:00Z", 600) - assert "callback_body" in job - assert "body" in job["callback_body"] - assert "source_endpoint" in job["callback_body"]["body"] - assert "destination_endpoint" in job["callback_body"]["body"] - assert job["callback_body"]["body"]["source_endpoint"] == GO_EP1_ID - assert job["callback_body"]["body"]["destination_endpoint"] == GO_EP2_ID - - -@pytest.mark.parametrize( - "badkey, value", (("submission_id", "foo"), ("skip_activation_check", True)) -) -def test_timer_from_transfer_data_rejects_forbidden_keys(badkey, value): - tdata = TransferData(GO_EP1_ID, GO_EP2_ID, **{badkey: value}) - with pytest.raises(ValueError): - with pytest.warns(exc.RemovedInV4Warning, match="Prefer TransferTimer"): - TimerJob.from_transfer_data(tdata, "2022-01-01T00:00:00Z", 600) - - def test_transfer_timer_ok(): tdata = TransferData(GO_EP1_ID, GO_EP2_ID) timer = TransferTimer(body=tdata, name="foo timer", schedule={"type": "once"}) From d31086746c3e92d74d0c7f9d0cd1d14f42e0a8ec Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 18 Jul 2025 22:55:11 +0000 Subject: [PATCH 110/176] (actions) update PR references --- .../20250718_155657_sirosen_remove_from_transfer_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst b/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst index af4444738..68dc1c7ec 100644 --- a/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst +++ b/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst @@ -3,4 +3,4 @@ Breaking Changes - The ``TimerJob.from_transfer_data`` classmethod, which was deprecated in globus-sdk version 3, has been removed. Users should use the ``TransferTimer`` - class to construct timers which submit transfer tasks. (:pr:`NUMBER`) + class to construct timers which submit transfer tasks. (:pr:`1269`) From fec898e507e703ce6f71e86e02ca4f09ba2e1b54 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 18 Jul 2025 18:04:48 -0500 Subject: [PATCH 111/176] Remove the 'oauth2_userinfo' alias Also remove the aliased test data for it. --- ...0718_180517_sirosen_remove_oauth2_userinfo.rst | 5 +++++ .../list_and_create_projects.py | 2 +- .../auth_manage_projects/manage_projects.py | 2 +- .../services/auth/client/service_client.py | 13 ++----------- .../testing/data/auth/oauth2_userinfo.py | 5 ----- .../services/auth/service_client/test_userinfo.py | 15 +-------------- 6 files changed, 10 insertions(+), 32 deletions(-) create mode 100644 changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst delete mode 100644 src/globus_sdk/testing/data/auth/oauth2_userinfo.py diff --git a/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst b/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst new file mode 100644 index 000000000..0805a1bd9 --- /dev/null +++ b/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst @@ -0,0 +1,5 @@ +Breaking Changes +---------------- + +- Removed ``AuthClient.oauth2_userinfo``. This method was deprecated in + ``globus-sdk`` version 3. (:pr:`NUMBER`) diff --git a/docs/examples/auth_manage_projects/list_and_create_projects.py b/docs/examples/auth_manage_projects/list_and_create_projects.py index 24ed32ef4..12f7cfdc7 100644 --- a/docs/examples/auth_manage_projects/list_and_create_projects.py +++ b/docs/examples/auth_manage_projects/list_and_create_projects.py @@ -38,7 +38,7 @@ def get_auth_client(): def create_project(args): auth_client = get_auth_client() - userinfo = auth_client.oauth2_userinfo() + userinfo = auth_client.userinfo() print( auth_client.create_project( args.name, diff --git a/docs/examples/auth_manage_projects/manage_projects.py b/docs/examples/auth_manage_projects/manage_projects.py index 02dc128fd..3c735304c 100644 --- a/docs/examples/auth_manage_projects/manage_projects.py +++ b/docs/examples/auth_manage_projects/manage_projects.py @@ -59,7 +59,7 @@ def get_auth_client(): def create_project(args): auth_client = get_auth_client() - userinfo = auth_client.oauth2_userinfo() + userinfo = auth_client.userinfo() print( auth_client.create_project( args.name, diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index ca1b2b5f6..e98d210b6 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -213,15 +213,6 @@ def userinfo(self) -> GlobusHTTPResponse: log.debug("Looking up OIDC-style Userinfo from Globus Auth") return self.get("/v2/oauth2/userinfo") - def oauth2_userinfo(self) -> GlobusHTTPResponse: - """ - A deprecated alias for ``userinfo``. - """ - exc.warn_deprecated( - "The method `oauth2_userinfo` is deprecated. Use `userinfo` instead." - ) - return self.userinfo() - def get_identities( self, *, @@ -561,7 +552,7 @@ def create_project( .. code-block:: pycon >>> ac = globus_sdk.AuthClient(...) - >>> userinfo = ac.oauth2_userinfo() + >>> userinfo = ac.userinfo() >>> identity_id = userinfo["sub"] >>> email = userinfo["email"] >>> r = ac.create_project( @@ -624,7 +615,7 @@ def update_project( >>> ac = globus_sdk.AuthClient(...) >>> project_id = ... - >>> userinfo = ac.oauth2_userinfo() + >>> userinfo = ac.userinfo() >>> email = userinfo["email"] >>> r = ac.update_project(project_id, contact_email=email) diff --git a/src/globus_sdk/testing/data/auth/oauth2_userinfo.py b/src/globus_sdk/testing/data/auth/oauth2_userinfo.py deleted file mode 100644 index 5f7b2190b..000000000 --- a/src/globus_sdk/testing/data/auth/oauth2_userinfo.py +++ /dev/null @@ -1,5 +0,0 @@ -# this is a clone of the userinfo.py data for compatibility across testing -# it should be removed in a future release -from .userinfo import RESPONSES - -__all__ = ("RESPONSES",) diff --git a/tests/functional/services/auth/service_client/test_userinfo.py b/tests/functional/services/auth/service_client/test_userinfo.py index 5d8ff6263..c6dfb9e5a 100644 --- a/tests/functional/services/auth/service_client/test_userinfo.py +++ b/tests/functional/services/auth/service_client/test_userinfo.py @@ -12,7 +12,7 @@ def test_userinfo(): @pytest.mark.parametrize("casename", ("unauthorized", "forbidden")) def test_userinfo_error_handling(service_client, casename): - meta = load_response(service_client.oauth2_userinfo, case=casename).metadata + meta = load_response(service_client.userinfo, case=casename).metadata with pytest.raises(globus_sdk.AuthAPIError) as excinfo: service_client.userinfo() @@ -21,16 +21,3 @@ def test_userinfo_error_handling(service_client, casename): assert err.http_status == meta["http_status"] assert err.code == meta["code"] assert err.request_id == meta["error_id"] - - -def test_oauth2_userinfo_warns(service_client): - # TODO: - # if the above success case is added, this test can be changed to use it - # that would let us get rid of the try-except guard below - load_response(service_client.oauth2_userinfo, case="unauthorized") - - with pytest.warns(globus_sdk.RemovedInV4Warning, match="Use `userinfo` instead."): - try: - service_client.oauth2_userinfo() - except globus_sdk.AuthAPIError: - pass From 696278e42390ed2f3fbb04132cbe23d257b1ba0d Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 18 Jul 2025 22:55:11 +0000 Subject: [PATCH 112/176] Remove `ConfidentialAppAuthClient.get_identities` --- ...81610_sirosen_remove_cc_get_identities.rst | 13 +++++ .../auth/client/confidential_client.py | 51 +------------------ src/globus_sdk/services/auth/identity_map.py | 4 +- 3 files changed, 17 insertions(+), 51 deletions(-) create mode 100644 changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst diff --git a/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst b/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst new file mode 100644 index 000000000..e8a579c58 --- /dev/null +++ b/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst @@ -0,0 +1,13 @@ +Breaking Changes +---------------- + +- Removed support for ``ConfidentialAppAuthClient.get_identities``. + This usage was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) + + - Users calling the Get Identities API on behalf of a client identity should + instead get tokens for the client and use those tokens to call + ``AuthClient.get_identities``. For example, by instantiating an + ``AuthClient`` using a ``ClientCredentialsAuthorizer``. + + - This also means that it is no longer valid to use a + ``ConfidentialAppAuthClient`` to initialize an ``IdentityMap``. diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 35b9d683c..decb6a7b5 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -5,7 +5,7 @@ import uuid from globus_sdk import exc -from globus_sdk._internal.remarshal import commajoin, strseq_iter, strseq_listify +from globus_sdk._internal.remarshal import strseq_iter, strseq_listify from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse @@ -13,11 +13,7 @@ from .._common import stringify_requested_scopes from ..flow_managers import GlobusAuthorizationCodeFlowManager -from ..response import ( - GetIdentitiesResponse, - OAuthClientCredentialsResponse, - OAuthDependentTokenResponse, -) +from ..response import OAuthClientCredentialsResponse, OAuthDependentTokenResponse from .base_login_client import AuthLoginClient log = logging.getLogger(__name__) @@ -63,49 +59,6 @@ def __init__( transport_params=transport_params, ) - def get_identities( - self, - *, - usernames: t.Iterable[str] | str | MissingType = MISSING, - ids: t.Iterable[uuid.UUID | str] | uuid.UUID | str | MissingType = MISSING, - provision: bool = False, - query_params: dict[str, t.Any] | None = None, - ) -> GetIdentitiesResponse: - """ - Perform a call to the Get Identities API using the direct client - credentials of this client. - - This method is considered deprecated -- callers should instead use client - credentials to get a token and then use that token to call the API via a - :class:`~.AuthClient`. - - :param usernames: A username or list of usernames to lookup. Mutually exclusive - with ``ids`` - :param ids: An identity ID or list of IDs to lookup. Mutually exclusive - with ``usernames`` - :param provision: Create identities if they do not exist, allowing clients to - get username-to-identity mappings prior to the identity being used - :param query_params: Any additional parameters to be passed through - as query params. - """ - exc.warn_deprecated( - "ConfidentialAuthClient.get_identities() is deprecated. " - "Get a token via `oauth2_client_credentials_tokens` " - "and use that to call the API instead." - ) - query_params = { - "usernames": commajoin(usernames), - # only specify `provision` if `usernames` is given - "provision": ( - str(provision).lower() if usernames is not MISSING else MISSING - ), - "ids": commajoin(ids), - **(query_params or {}), - } - return GetIdentitiesResponse( - self.get("/v2/api/identities", query_params=query_params) - ) - def oauth2_client_credentials_tokens( self, requested_scopes: str | Scope | t.Iterable[str | Scope] ) -> OAuthClientCredentialsResponse: diff --git a/src/globus_sdk/services/auth/identity_map.py b/src/globus_sdk/services/auth/identity_map.py index fe8fe5c52..0e939541b 100644 --- a/src/globus_sdk/services/auth/identity_map.py +++ b/src/globus_sdk/services/auth/identity_map.py @@ -3,7 +3,7 @@ import typing as t import uuid -from .client import AuthClient, ConfidentialAppAuthClient +from .client import AuthClient def is_username(val: str) -> bool: @@ -131,7 +131,7 @@ class IdentityMap: def __init__( self, - auth_client: AuthClient | ConfidentialAppAuthClient, + auth_client: AuthClient, identity_ids: t.Iterable[str] | None = None, *, id_batch_size: int | None = None, From 747d39429694155e84c7a4fed04675d9cebb62a8 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 19 Jul 2025 14:06:02 +0000 Subject: [PATCH 113/176] (actions) update PR references --- changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst b/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst index 0805a1bd9..4f40f2c4f 100644 --- a/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst +++ b/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst @@ -2,4 +2,4 @@ Breaking Changes ---------------- - Removed ``AuthClient.oauth2_userinfo``. This method was deprecated in - ``globus-sdk`` version 3. (:pr:`NUMBER`) + ``globus-sdk`` version 3. (:pr:`1272`) From d87d87a624aa34aaa4f89f95b5311d47865f0e7a Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 21 Jul 2025 15:50:46 +0000 Subject: [PATCH 114/176] (actions) update PR references --- changelog.d/20250718_155001_sirosen_rename_function_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250718_155001_sirosen_rename_function_data.rst b/changelog.d/20250718_155001_sirosen_rename_function_data.rst index 775e95e64..de3806e9b 100644 --- a/changelog.d/20250718_155001_sirosen_rename_function_data.rst +++ b/changelog.d/20250718_155001_sirosen_rename_function_data.rst @@ -2,4 +2,4 @@ Breaking Changes ---------------- - The ``function_data`` argument to ``ComputeClientV2.register_function`` has - been renamed to ``data`` to be consistent with other usages. (:pr:`NUMBER`) + been renamed to ``data`` to be consistent with other usages. From bba8edccdd175026f550707d9abb61e102398120 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 18 Jul 2025 18:29:22 -0500 Subject: [PATCH 115/176] Document 'get_identities' upgrading process Focus on `ClientApp` as the lowest-effort path. --- docs/upgrading.rst | 30 +++++++++++++++++++ .../mypy-ignore-tests/identity_map.py | 4 +-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7d9c97f9f..f8fbd41c3 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -112,6 +112,36 @@ To control when a submission ID is fetched, use submission_id=submission_id, ) + +``ConfidentialAppAuthClient`` Cannot Directly Call ``get_identities`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Users of client identities are now required to get tokens in order to use the +Get Identities API, and will need to use the ``AuthClient`` class for this +purpose. +This can most simply be managed by use of a ``ClientApp`` to automatically +fetch the appropriate tokens. + +Update usage like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk import ConfidentialAppAuthClient + + client = ConfidentialAppAuthClient(CLIENT_ID, CLIENT_SECRET) + + identities = client.get_identities(usernames="globus@globus.org") + + # globus-sdk v4 + from globus_sdk import ClientApp, AuthClient + + app = ClientApp(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) + client = AuthClient(app=app) + + identities = client.get_identities(usernames="globus@globus.org") + + Scope Constants Are Now Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/non-pytest/mypy-ignore-tests/identity_map.py b/tests/non-pytest/mypy-ignore-tests/identity_map.py index a64f1d865..ac5fed610 100644 --- a/tests/non-pytest/mypy-ignore-tests/identity_map.py +++ b/tests/non-pytest/mypy-ignore-tests/identity_map.py @@ -6,9 +6,9 @@ nc = globus_sdk.NativeAppAuthClient("foo_client_id") cc = globus_sdk.ConfidentialAppAuthClient("foo_client_id", "foo_client_secret") -# check init allows CC, but not NC +# check init allows the service client but not the login clients im = globus_sdk.IdentityMap(ac) -im = globus_sdk.IdentityMap(cc) +im = globus_sdk.IdentityMap(cc) # type: ignore[arg-type] im = globus_sdk.IdentityMap(nc) # type: ignore[arg-type] # getitem and delitem work, but setitem and contains do not From 46026bdbdfdbec97ba13e3c50043cbea3164432e Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 22 Jul 2025 15:16:34 -0500 Subject: [PATCH 116/176] Remove 'TransferClient.create_endpoint' This method is deprecated in SDK version 3. --- ...22_150116_sirosen_remove_gcsv4_methods.rst | 6 +++++ src/globus_sdk/services/transfer/client.py | 19 --------------- .../testing/data/transfer/create_endpoint.py | 22 ------------------ .../services/transfer/test_simple.py | 23 ------------------- 4 files changed, 6 insertions(+), 64 deletions(-) create mode 100644 changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst delete mode 100644 src/globus_sdk/testing/data/transfer/create_endpoint.py diff --git a/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst b/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst new file mode 100644 index 000000000..4b96ede38 --- /dev/null +++ b/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst @@ -0,0 +1,6 @@ +Removed +------- + +- ``TransferClient.create_endpoint`` has been removed. This method primarily + supported creation of GCSv4 servers and was deprecated in ``globus-sdk`` v3. + (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 322b9dd42..26f79301b 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -411,25 +411,6 @@ def set_subscription_admin_verified( data={"subscription_admin_verified": subscription_admin_verified}, ) - def create_endpoint(self, data: dict[str, t.Any]) -> response.GlobusHTTPResponse: - """ - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param data: An endpoint document with fields for the new endpoint - """ - if data.get("myproxy_server") and data.get("oauth_server"): - raise exc.GlobusSDKUsageError( - "an endpoint cannot be created using multiple identity " - "providers for activation; specify either MyProxy or OAuth, " - "not both" - ) - - log.debug("TransferClient.create_endpoint(...)") - return self.post("/v0.10/endpoint", data=data) - def delete_endpoint( self, endpoint_id: uuid.UUID | str ) -> response.GlobusHTTPResponse: diff --git a/src/globus_sdk/testing/data/transfer/create_endpoint.py b/src/globus_sdk/testing/data/transfer/create_endpoint.py deleted file mode 100644 index 4b8fa534a..000000000 --- a/src/globus_sdk/testing/data/transfer/create_endpoint.py +++ /dev/null @@ -1,22 +0,0 @@ -from globus_sdk.testing.models import RegisteredResponse, ResponseSet - -from ._common import ENDPOINT_ID - -RESPONSES = ResponseSet( - metadata={"endpoint_id": ENDPOINT_ID}, - default=RegisteredResponse( - service="transfer", - method="POST", - path="/v0.10/endpoint", - json={ - "DATA_TYPE": "endpoint_create_result", - "display_name": "my cool endpoint", - "code": "Created", - "globus_connect_setup_key": None, - "id": ENDPOINT_ID, - "message": "Endpoint created successfully", - "request_id": "d4MqMwFJ9", - "resource": "/v0.10/endpoint", - }, - ), -) diff --git a/tests/functional/services/transfer/test_simple.py b/tests/functional/services/transfer/test_simple.py index ce670db3a..67da61ccc 100644 --- a/tests/functional/services/transfer/test_simple.py +++ b/tests/functional/services/transfer/test_simple.py @@ -77,29 +77,6 @@ def test_update_endpoint_invalid_activation_servers(client): assert "either MyProxy or OAuth, not both" in str(excinfo.value) -def test_create_endpoint(client): - load_response(client.create_endpoint) - - create_data = {"display_name": "Name", "description": "desc"} - create_doc = client.create_endpoint(create_data) - - # make sure response is a successful update - assert create_doc["DATA_TYPE"] == "endpoint_create_result" - assert create_doc["code"] == "Created" - assert create_doc["message"] == "Endpoint created successfully" - - req = get_last_request() - assert json.loads(req.body) == create_data - - -def test_create_endpoint_invalid_activation_servers(client): - create_data = {"oauth_server": "foo", "myproxy_server": "bar"} - with pytest.raises(globus_sdk.GlobusSDKUsageError) as excinfo: - client.create_endpoint(create_data) - - assert "either MyProxy or OAuth, not both" in str(excinfo.value) - - def test_autoactivation(client): """ Do `autoactivate` on go#ep1, validate results, and check that `if_expires_in` can be From c1145bf76f65672ed24a9d5c4064865dfd3b3dd9 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 22 Jul 2025 16:31:03 -0500 Subject: [PATCH 117/176] Remove `GCSClient.connector_id_to_name` --- .../20250722_162951_sirosen_4_x_dev.rst | 6 ++++ src/globus_sdk/services/gcs/client.py | 31 +------------------ .../unit/helpers/gcs/test_connector_table.py | 8 +---- 3 files changed, 8 insertions(+), 37 deletions(-) create mode 100644 changelog.d/20250722_162951_sirosen_4_x_dev.rst diff --git a/changelog.d/20250722_162951_sirosen_4_x_dev.rst b/changelog.d/20250722_162951_sirosen_4_x_dev.rst new file mode 100644 index 000000000..b560c1bbb --- /dev/null +++ b/changelog.d/20250722_162951_sirosen_4_x_dev.rst @@ -0,0 +1,6 @@ +Removed +------- + +- ``GCSClient.connector_id_to_name()`` has been removed. It was deprecated in + ``globus-sdk`` version 3. Users should use ``globus_sdk.ConnectorTable`` + instead. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index 922d7aefb..c05a1cfe7 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -3,7 +3,7 @@ import typing as t import uuid -from globus_sdk import client, exc, paging, response +from globus_sdk import client, paging, response from globus_sdk._internal.classprop import classproperty from globus_sdk._internal.remarshal import commajoin from globus_sdk._internal.utils import slash_join @@ -12,7 +12,6 @@ from globus_sdk.globus_app import GlobusApp from globus_sdk.scopes import GCSCollectionScopes, GCSEndpointScopes, Scope -from .connector_table import ConnectorTable from .data import ( CollectionDocument, EndpointDocument, @@ -108,34 +107,6 @@ def get_gcs_collection_scopes( """ return GCSCollectionScopes(str(collection_id)) - @staticmethod - def connector_id_to_name(connector_id: uuid.UUID | str) -> str | None: - """ - .. warning:: - - This method is deprecated -- use - ``ConnectorTable.lookup`` instead. - - Helper that converts a given connector ID into a human-readable - connector name string. - - :param connector_id: The ID of the connector - """ - exc.warn_deprecated( - "`connector_id_to_name` has been replaced with " - "`ConnectorTable.lookup`. Use that instead, " - "and retrieve the `name` attribute from the result." - ) - connector_obj = ConnectorTable.lookup(connector_id) - if connector_obj is None: - return None - name = connector_obj.name - # compatibility shim due to name change in the data (which was updated to - # match internal sources referring to this only as "BlackPearl") - if name == "BlackPearl": - name = "Spectralogic BlackPearl" - return name - @property def default_scope_requirements(self) -> list[Scope]: return [ diff --git a/tests/unit/helpers/gcs/test_connector_table.py b/tests/unit/helpers/gcs/test_connector_table.py index 3deaf8a48..9f8e16a8c 100644 --- a/tests/unit/helpers/gcs/test_connector_table.py +++ b/tests/unit/helpers/gcs/test_connector_table.py @@ -6,13 +6,7 @@ import pytest -from globus_sdk import ConnectorTable, GCSClient, GlobusConnectServerConnector, exc - - -def test_deprecated_connector_lookup_method_warns(): - client = GCSClient("foo.bar.example.org") - with pytest.warns(exc.RemovedInV4Warning): - assert client.connector_id_to_name("foo") is None +from globus_sdk import ConnectorTable, GlobusConnectServerConnector @pytest.mark.parametrize("connector_data", ConnectorTable._connectors) From a4b64f0e324e1cf0e6036442f9a8bc9f3efb0cea Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 22 Jul 2025 21:58:11 +0000 Subject: [PATCH 118/176] (actions) update PR references --- changelog.d/20250722_162951_sirosen_4_x_dev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250722_162951_sirosen_4_x_dev.rst b/changelog.d/20250722_162951_sirosen_4_x_dev.rst index b560c1bbb..b722ce03f 100644 --- a/changelog.d/20250722_162951_sirosen_4_x_dev.rst +++ b/changelog.d/20250722_162951_sirosen_4_x_dev.rst @@ -3,4 +3,4 @@ Removed - ``GCSClient.connector_id_to_name()`` has been removed. It was deprecated in ``globus-sdk`` version 3. Users should use ``globus_sdk.ConnectorTable`` - instead. (:pr:`NUMBER`) + instead. (:pr:`1277`) From 022303a3c0238476290b09b7e405971ecc9d2db5 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 22 Jul 2025 21:58:32 +0000 Subject: [PATCH 119/176] (actions) update PR references --- changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst b/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst index 4b96ede38..aefc1bd17 100644 --- a/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst +++ b/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst @@ -3,4 +3,4 @@ Removed - ``TransferClient.create_endpoint`` has been removed. This method primarily supported creation of GCSv4 servers and was deprecated in ``globus-sdk`` v3. - (:pr:`NUMBER`) + (:pr:`1276`) From dcba881448508ce8ced3e10edd504d0062f7881f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 22 Jul 2025 21:59:25 +0000 Subject: [PATCH 120/176] (actions) update PR references --- .../20250718_181610_sirosen_remove_cc_get_identities.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst b/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst index e8a579c58..62bf3d984 100644 --- a/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst +++ b/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst @@ -2,7 +2,7 @@ Breaking Changes ---------------- - Removed support for ``ConfidentialAppAuthClient.get_identities``. - This usage was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) + This usage was deprecated in ``globus-sdk`` version 3. (:pr:`1273`) - Users calling the Get Identities API on behalf of a client identity should instead get tokens for the client and use those tokens to call From 9bb4f53052bb83f368e25d9d61803ec94a39669a Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 23 Jul 2025 13:06:08 -0500 Subject: [PATCH 121/176] Remove 'client_id' from 'AuthClient' (#1271) --- ...n_auth_service_client_remove_client_id.rst | 5 +++ .../services/auth/client/service_client.py | 33 ----------------- .../auth_client_initialization.py | 7 ++++ .../auth_client_jwk_methods.py | 2 - tests/unit/test_auth_clients.py | 37 ++----------------- 5 files changed, 15 insertions(+), 69 deletions(-) create mode 100644 changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst diff --git a/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst b/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst new file mode 100644 index 000000000..7b59b27f9 --- /dev/null +++ b/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst @@ -0,0 +1,5 @@ +Breaking Changes +---------------- + +- ``AuthClient`` no longer accepts ``client_id`` as a parameter and does not + provide it as an attribute. This was deprecated in globus-sdk version 3. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index e98d210b6..b927ce60f 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -72,7 +72,6 @@ class AuthClient(client.BaseClient): def __init__( self, - client_id: uuid.UUID | str | None = None, environment: str | None = None, base_url: str | None = None, app: GlobusApp | None = None, @@ -91,37 +90,6 @@ def __init__( transport_params=transport_params, ) - self._client_id = str(client_id) if client_id is not None else None - if client_id is not None: - exc.warn_deprecated( - "The client_id parameter is no longer accepted by `AuthClient` / " - "`AuthClient`. When creating a client which represents an " - "application, use `NativeAppAuthClient` or " - "`ConfidentialAppAuthClient` instead." - ) - - # this attribute is preserved for compatibility, but will be removed in a - # future release - @property - def client_id(self) -> str | None: - exc.warn_deprecated( - "The client_id attribute on `AuthClient` / " - "`AuthClient` is deprecated. " - "For clients with client IDs, use `NativeAppAuthClient` or " - "`ConfidentialAppAuthClient` instead." - ) - return self._client_id - - @client_id.setter - def client_id(self, value: uuid.UUID | str) -> None: - exc.warn_deprecated( - "The client_id attribute on `AuthClient` / " - "`AuthClient` is deprecated. " - "For clients with client IDs, use `NativeAppAuthClient` or " - "`ConfidentialAppAuthClient` instead." - ) - self._client_id = str(value) if value is not None else None - # FYI: this get_openid_configuration method is duplicated in AuthLoginBaseClient # if this code is modified, please update that copy as well # this will ideally be resolved in a future SDK version by making this the only copy @@ -807,7 +775,6 @@ def create_policy( .. code-block:: pycon >>> ac = globus_sdk.AuthClient(...) - >>> client_id = ... >>> r = ac.create_policy( ... project_id="da84e531-1afb-43cb-8c87-135ab580516a", ... high_assurance=True, diff --git a/tests/non-pytest/mypy-ignore-tests/auth_client_initialization.py b/tests/non-pytest/mypy-ignore-tests/auth_client_initialization.py index 2215aa2e7..59adbc305 100644 --- a/tests/non-pytest/mypy-ignore-tests/auth_client_initialization.py +++ b/tests/non-pytest/mypy-ignore-tests/auth_client_initialization.py @@ -16,3 +16,10 @@ cc = globus_sdk.ConfidentialAppAuthClient( # type: ignore[call-arg] "foo_client_id", "foo_client_secret", authorizer=authorizer ) + +# the login clients allow a client_id kwarg, but AuthClient does not +globus_sdk.NativeAppAuthClient(client_id="foo_client_id") +globus_sdk.ConfidentialAppAuthClient( + client_id="foo_client_id", client_secret="foo_client_secret" +) +globus_sdk.AuthClient(client_id="foo_client_id") # type: ignore[call-arg] diff --git a/tests/non-pytest/mypy-ignore-tests/auth_client_jwk_methods.py b/tests/non-pytest/mypy-ignore-tests/auth_client_jwk_methods.py index f5456b062..f2df2f570 100644 --- a/tests/non-pytest/mypy-ignore-tests/auth_client_jwk_methods.py +++ b/tests/non-pytest/mypy-ignore-tests/auth_client_jwk_methods.py @@ -2,12 +2,10 @@ from globus_sdk.services.auth._common import SupportsJWKMethods # setup clients -ac = globus_sdk.AuthClient() nc = globus_sdk.NativeAppAuthClient("foo_client_id") cc = globus_sdk.ConfidentialAppAuthClient("foo_client_id", "foo_client_secret") # check that each one supports the JWK methods x: SupportsJWKMethods -x = ac x = nc x = cc diff --git a/tests/unit/test_auth_clients.py b/tests/unit/test_auth_clients.py index 8e78b0056..fc585a584 100644 --- a/tests/unit/test_auth_clients.py +++ b/tests/unit/test_auth_clients.py @@ -9,39 +9,6 @@ CLIENT_ID_STR = str(CLIENT_ID_UUID) -def test_service_client_does_not_require_client_id(): - client = globus_sdk.AuthClient() - # accessing the attribute warns, but provides None - with pytest.warns(globus_sdk.RemovedInV4Warning): - assert client.client_id is None - - -pass_value_params = pytest.mark.parametrize( - "pass_value", (CLIENT_ID_STR, CLIENT_ID_UUID), ids=("str", "uuid") -) - - -@pass_value_params -def test_service_client_allows_client_id_but_warns(pass_value): - # init will warn because a value is being passed - with pytest.warns(globus_sdk.RemovedInV4Warning): - client = globus_sdk.AuthClient(client_id=pass_value) - - # accessing the attribute warns a second time, but provides the stringified value - with pytest.warns(globus_sdk.RemovedInV4Warning): - assert client.client_id == CLIENT_ID_STR - - -@pass_value_params -def test_service_client_allows_client_id_assignment(pass_value): - client = globus_sdk.AuthClient() - with pytest.warns(globus_sdk.RemovedInV4Warning): - client.client_id = pass_value - - with pytest.warns(globus_sdk.RemovedInV4Warning): - assert client.client_id == CLIENT_ID_STR - - @pytest.mark.parametrize( "client_type", ( @@ -50,7 +17,9 @@ def test_service_client_allows_client_id_assignment(pass_value): globus_sdk.NativeAppAuthClient, ), ) -@pass_value_params +@pytest.mark.parametrize( + "pass_value", (CLIENT_ID_STR, CLIENT_ID_UUID), ids=("str", "uuid") +) def test_can_use_uuid_or_str_for_client_id(client_type, pass_value): if client_type in (globus_sdk.AuthLoginClient, globus_sdk.NativeAppAuthClient): client = client_type(client_id=pass_value) From 3c8b81ced3cb2bb66e9da02e341fe45ef62818fb Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 23 Jul 2025 18:06:20 +0000 Subject: [PATCH 122/176] (actions) update PR references --- ...0718_175205_sirosen_auth_service_client_remove_client_id.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst b/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst index 7b59b27f9..8f694bc65 100644 --- a/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst +++ b/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst @@ -2,4 +2,4 @@ Breaking Changes ---------------- - ``AuthClient`` no longer accepts ``client_id`` as a parameter and does not - provide it as an attribute. This was deprecated in globus-sdk version 3. (:pr:`NUMBER`) + provide it as an attribute. This was deprecated in globus-sdk version 3. (:pr:`1271`) From de085f248e8b09f5e869e73437a12a07fca67c22 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 23 Jul 2025 16:06:52 -0500 Subject: [PATCH 123/176] Remove Endpoint Activation support (#1279) --- ...23_110332_sirosen_remove_gcsv4_methods.rst | 17 ++ docs/services/transfer.rst | 4 - src/globus_sdk/__init__.pyi | 2 - src/globus_sdk/services/transfer/__init__.py | 3 +- src/globus_sdk/services/transfer/client.py | 133 +--------------- .../services/transfer/response/__init__.py | 3 +- .../services/transfer/response/activation.py | 148 ------------------ .../activation_already_activated_stub.json | 3 - .../fixture_data/activation_stub.json | 3 - .../services/transfer/test_simple.py | 78 --------- .../responses/test_activation_response.py | 117 -------------- 11 files changed, 20 insertions(+), 491 deletions(-) create mode 100644 changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst delete mode 100644 src/globus_sdk/services/transfer/response/activation.py delete mode 100644 tests/functional/services/transfer/fixture_data/activation_already_activated_stub.json delete mode 100644 tests/functional/services/transfer/fixture_data/activation_stub.json delete mode 100644 tests/unit/responses/test_activation_response.py diff --git a/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst b/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst new file mode 100644 index 000000000..f41fc9490 --- /dev/null +++ b/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst @@ -0,0 +1,17 @@ +Removed +------- + +- Removed support for Endpoint Activation, a feature which was specific to + Globus Connect Server v4. (:pr:`NUMBER`) + + - Removed the activation methods: ``TransferClient.endpoint_autoactivate``, + ``TransferClient.endpoint_activate``, + ``TransferClient.endpoint_deactivate``, and + ``TransferClient.endpoint_get_activation_requirements`` + + - Removed the specialized ``ActivationRequirementsResponse`` parsed response + type + + - ``TransferClient.update_endpoint`` would previously check the + ``myproxy_server`` and ``oauth_server`` parameters, which were solely used + for the purpose of configuring activation. It no longer does so. diff --git a/docs/services/transfer.rst b/docs/services/transfer.rst index 9bd2c90ce..f79624585 100644 --- a/docs/services/transfer.rst +++ b/docs/services/transfer.rst @@ -42,10 +42,6 @@ error, rather than a generic :class:`GlobusAPIError`. Transfer Responses ------------------ -.. autoclass:: ActivationRequirementsResponse - :members: - :show-inheritance: - .. autoclass:: IterableTransferResponse :members: :show-inheritance: diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index bccd1ad80..a74e41c76 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -119,7 +119,6 @@ from .services.timers import ( TransferTimer, ) from .services.transfer import ( - ActivationRequirementsResponse, DeleteData, IterableTransferResponse, TransferAPIError, @@ -238,7 +237,6 @@ __all__ = ( "TimersAPIError", "TimersClient", "TransferTimer", - "ActivationRequirementsResponse", "DeleteData", "IterableTransferResponse", "TransferAPIError", diff --git a/src/globus_sdk/services/transfer/__init__.py b/src/globus_sdk/services/transfer/__init__.py index 8547712b4..2fa33d11a 100644 --- a/src/globus_sdk/services/transfer/__init__.py +++ b/src/globus_sdk/services/transfer/__init__.py @@ -1,13 +1,12 @@ from .client import TransferClient from .data import DeleteData, TransferData from .errors import TransferAPIError -from .response import ActivationRequirementsResponse, IterableTransferResponse +from .response import IterableTransferResponse __all__ = ( "TransferClient", "TransferData", "DeleteData", "TransferAPIError", - "ActivationRequirementsResponse", "IterableTransferResponse", ) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index a50553a4b..cf79d03ff 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -14,7 +14,7 @@ from .data import DeleteData, TransferData from .errors import TransferAPIError -from .response import ActivationRequirementsResponse, IterableTransferResponse +from .response import IterableTransferResponse from .transport import TransferRequestsTransport log = logging.getLogger(__name__) @@ -281,18 +281,6 @@ def update_endpoint( .. extdoclink:: Update Globus Connect Personal collection by id :ref: transfer/gcp_management/#update_collection_by_id """ # noqa: E501 - if data.get("myproxy_server"): - if data.get("oauth_server"): - raise exc.GlobusSDKUsageError( - "an endpoint cannot be reconfigured to use multiple " - "identity providers for activation; specify either " - "MyProxy or OAuth, not both" - ) - else: - data["oauth_server"] = None - elif data.get("oauth_server"): - data["myproxy_server"] = None - log.debug(f"TransferClient.update_endpoint({endpoint_id}, ...)") return self.put( f"/v0.10/endpoint/{endpoint_id}", data=data, query_params=query_params @@ -549,125 +537,6 @@ def endpoint_search( self.get("/v0.10/endpoint_search", query_params=query_params) ) - def endpoint_autoactivate( - self, - endpoint_id: uuid.UUID | str, - *, - if_expires_in: int | MissingType = MISSING, - query_params: dict[str, t.Any] | None = None, - ) -> response.GlobusHTTPResponse: - r""" - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param endpoint_id: The ID of the endpoint to autoactivate - :param if_expires_in: A number of seconds. Autoactivation will only be attempted - if the current activation expires within this timeframe. Otherwise, - autoactivation will succeed with a code of 'AlreadyActivated' - :param query_params: Any additional parameters will be passed through - as query params. - """ # noqa: E501 - exc.warn_deprecated( - "endpoint_autoactivate is specific to Globus Connect Server v4, " - "which is no longer supported by the Transfer API." - ) - query_params = { - "if_expires_in": if_expires_in, - **(query_params or {}), - } - log.debug(f"TransferClient.endpoint_autoactivate({endpoint_id})") - return self.post( - f"/v0.10/endpoint/{endpoint_id}/autoactivate", query_params=query_params - ) - - def endpoint_deactivate( - self, - endpoint_id: uuid.UUID | str, - *, - query_params: dict[str, t.Any] | None = None, - ) -> response.GlobusHTTPResponse: - """ - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param endpoint_id: The ID of the endpoint to deactivate - :param query_params: Any additional parameters will be passed through - as query params. - """ - exc.warn_deprecated( - "endpoint_deactivate is specific to Globus Connect Server v4, " - "which is no longer supported by the Transfer API." - ) - log.debug(f"TransferClient.endpoint_deactivate({endpoint_id})") - return self.post( - f"/v0.10/endpoint/{endpoint_id}/deactivate", query_params=query_params - ) - - def endpoint_activate( - self, - endpoint_id: uuid.UUID | str, - *, - requirements_data: dict[str, t.Any] | None, - query_params: dict[str, t.Any] | None = None, - ) -> response.GlobusHTTPResponse: - """ - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param endpoint_id: The ID of the endpoint to activate - :pram requirements_data: Filled in activation requirements data, as can be - fetched from :meth:`~endpoint_get_activation_requirements`. Only the fields - for the activation type being used need to be filled in. - :param requirements_data: An optional body for the request - :param query_params: Any additional parameters will be passed through - as query params. - """ - exc.warn_deprecated( - "endpoint_activate is specific to Globus Connect Server v4, " - "which is no longer supported by the Transfer API." - ) - log.debug(f"TransferClient.endpoint_activate({endpoint_id})") - return self.post( - f"/v0.10/endpoint/{endpoint_id}/activate", - data=requirements_data, - query_params=query_params, - ) - - def endpoint_get_activation_requirements( - self, - endpoint_id: uuid.UUID | str, - *, - query_params: dict[str, t.Any] | None = None, - ) -> ActivationRequirementsResponse: - """ - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param endpoint_id: The ID of the endpoint whose activation requirements data is - being looked up - :param query_params: Any additional parameters will be passed through - as query params. - """ - exc.warn_deprecated( - "endpoint_get_activation_requirements is specific to " - "Globus Connect Server v4, " - "which is no longer supported by the Transfer API." - ) - return ActivationRequirementsResponse( - self.get( - f"/v0.10/endpoint/{endpoint_id}/activation_requirements", - query_params=query_params, - ) - ) - def my_effective_pause_rule_list( self, endpoint_id: uuid.UUID | str, diff --git a/src/globus_sdk/services/transfer/response/__init__.py b/src/globus_sdk/services/transfer/response/__init__.py index 5c0ea61b9..27a012cf2 100644 --- a/src/globus_sdk/services/transfer/response/__init__.py +++ b/src/globus_sdk/services/transfer/response/__init__.py @@ -1,4 +1,3 @@ -from .activation import ActivationRequirementsResponse from .iterable import IterableTransferResponse -__all__ = ("IterableTransferResponse", "ActivationRequirementsResponse") +__all__ = ("IterableTransferResponse",) diff --git a/src/globus_sdk/services/transfer/response/activation.py b/src/globus_sdk/services/transfer/response/activation.py deleted file mode 100644 index 107a8b2a3..000000000 --- a/src/globus_sdk/services/transfer/response/activation.py +++ /dev/null @@ -1,148 +0,0 @@ -from __future__ import annotations - -import time -import typing as t - -from globus_sdk.response import GlobusHTTPResponse - - -class ActivationRequirementsResponse(GlobusHTTPResponse): - """ - Response class for Activation Requirements responses. - - All Activation Requirements documents refer to a specific Endpoint, from - whence they were acquired. References to "the Endpoint" implicitly refer to - that originating Endpoint, and not to some other Endpoint. - - **External Documentation** - - See - `Activation Requirements Document\ - `_ - in the API documentation for details. - """ - - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: - super().__init__(*args, **kwargs) - - # at initialization time, capture expires_in and convert to an absolute - # timestamp -- otherwise, the time between receiving the response and - # querying its status will start to matter - if self["expires_in"] == -1: - # expires_in=-1 is the "infinite lifetime" case - self.expires_at: int | None = None - else: - self.expires_at = int(time.time() + self["expires_in"]) - - @property - def supports_auto_activation(self) -> bool: - r""" - Check if the document lists Auto-Activation as an available type of - activation. - Typically good to use when you need to catch endpoints that require web - activation before proceeding. - - >>> endpoint_id = "..." - >>> tc = TransferClient(...) - >>> reqs_doc = tc.endpoint_get_activation_requirements(endpoint_id) - >>> if not reqs_doc.supports_auto_activation: - >>> # use `from __future__ import print_function` in py2 - >>> print(("This endpoint requires web activation. " - >>> "Please login and activate the endpoint here:\n" - >>> "https://app.globus.org/file-manager?origin_id={}") - >>> .format(endpoint_id), file=sys.stderr) - >>> # py3 calls it `input()` in py2, use `raw_input()` - >>> input("Please Hit Enter When You Are Done") - """ - return t.cast(bool, self["auto_activation_supported"]) - - @property - def supports_web_activation(self) -> bool: - """ - Check if the document lists known types of activation that can be done - through the web. If this returns ``False``, it means that the endpoint - is of a highly unusual type, and you should directly inspect the - response's ``data`` attribute to see what is required. Sending users to - the web page for activation is also a fairly safe action to take. - Note that ``ActivationRequirementsResponse.supports_auto_activation`` - directly implies - ``ActivationRequirementsResponse.supports_web_activation``, so these - are *not* exclusive. - - For example, - - >>> tc = TransferClient(...) - >>> reqs_doc = tc.endpoint_get_activation_requirements(...) - >>> if not reqs_doc.supports_web_activation: - >>> # use `from __future__ import print_function` in py2 - >>> print("Highly unusual endpoint. " + - >>> "Cannot webactivate. Raw doc: " + - >>> str(reqs_doc), file=sys.stderr) - >>> print("Sending user to web anyway, just in case.", - >>> file=sys.stderr) - >>> ... - """ - return ( - self.supports_auto_activation - or self["oauth_server"] is not None - or any( - x for x in self["DATA"] if x["type"] in ("myproxy", "delegate_myproxy") - ) - ) - - def active_until(self, time_seconds: int, relative_time: bool = True) -> bool: - """ - Check if the Endpoint will be active until some time in the future, - given as an integer number of seconds. - When ``relative_time=False``, the ``time_seconds`` is interpreted as a - POSIX timestamp. - - This supports queries using both relative and absolute timestamps to - better support a wide range of use cases. For example, if I have a task - that I know will typically take N seconds, and I want an M second - safety margin: - - >>> num_secs_allowed = N + M - >>> tc = TransferClient(...) - >>> reqs_doc = tc.endpoint_get_activation_requirements(...) - >>> if not reqs_doc.active_until(num_secs_allowed): - >>> raise Exception("Endpoint won't be active long enough") - >>> ... - - or, alternatively, if I know that the endpoint must be active until - October 18th, 2016 for my tasks to complete: - - >>> oct18_2016 = 1476803436 - >>> tc = TransferClient(...) - >>> reqs_doc = tc.endpoint_get_activation_requirements(...) - >>> if not reqs_doc.active_until(oct18_2016, relative_time=False): - >>> raise Exception("Endpoint won't be active long enough") - >>> ... - - :param time_seconds: Number of seconds into the future. - :param relative_time: Defaults to True. When False, ``time_seconds`` is treated - as a POSIX timestamp (i.e. seconds since epoch as an integer) instead of - its ordinary behavior. - - - :return: True if the Endpoint will be active until the deadline, False otherwise - """ - # inactive endpoint - if not self["activated"]: - return False - # infinite activation period - if self.expires_at is None: - return True - - if relative_time: - return (time.time() + time_seconds) < self.expires_at - else: - return time_seconds < self.expires_at - - @property - def always_activated(self) -> bool: - """ - Returns True if the endpoint activation never expires - (e.g. shared endpoints, globus connect personal endpoints). - """ - return t.cast(int, self["expires_in"]) == -1 diff --git a/tests/functional/services/transfer/fixture_data/activation_already_activated_stub.json b/tests/functional/services/transfer/fixture_data/activation_already_activated_stub.json deleted file mode 100644 index 33983b9aa..000000000 --- a/tests/functional/services/transfer/fixture_data/activation_already_activated_stub.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "code": "AlreadyActivated" -} diff --git a/tests/functional/services/transfer/fixture_data/activation_stub.json b/tests/functional/services/transfer/fixture_data/activation_stub.json deleted file mode 100644 index 3ebd407a6..000000000 --- a/tests/functional/services/transfer/fixture_data/activation_stub.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "code": "AutoActivated.CachedCredential" -} diff --git a/tests/functional/services/transfer/test_simple.py b/tests/functional/services/transfer/test_simple.py index b6a79f319..1e03b88be 100644 --- a/tests/functional/services/transfer/test_simple.py +++ b/tests/functional/services/transfer/test_simple.py @@ -1,13 +1,9 @@ import json -import urllib.parse import uuid import pytest -import globus_sdk -import globus_sdk.exc from globus_sdk.testing import get_last_request, load_response -from tests.common import GO_EP1_ID, register_api_route_fixture_file def test_get_endpoint(client): @@ -43,77 +39,3 @@ def test_update_endpoint(epid_type, client): req = get_last_request() assert json.loads(req.body) == update_data - - -def test_update_endpoint_rewrites_activation_servers(client): - """ - Update endpoint, validate results - """ - meta = load_response(client.update_endpoint).metadata - epid = meta["endpoint_id"] - - # sending myproxy_server implicitly adds oauth_server=null - update_data = {"myproxy_server": "foo"} - client.update_endpoint(epid, update_data.copy()) - req = get_last_request() - assert json.loads(req.body) != update_data - update_data["oauth_server"] = None - assert json.loads(req.body) == update_data - - # sending oauth_server implicitly adds myproxy_server=null - update_data = {"oauth_server": "foo"} - client.update_endpoint(epid, update_data.copy()) - req = get_last_request() - assert json.loads(req.body) != update_data - update_data["myproxy_server"] = None - assert json.loads(req.body) == update_data - - -def test_update_endpoint_invalid_activation_servers(client): - epid = "example-id" - update_data = {"oauth_server": "foo", "myproxy_server": "bar"} - with pytest.raises(globus_sdk.GlobusSDKUsageError) as excinfo: - client.update_endpoint(epid, update_data) - - assert "either MyProxy or OAuth, not both" in str(excinfo.value) - - -def test_autoactivation(client): - """ - Do `autoactivate` on go#ep1, validate results, and check that `if_expires_in` can be - passed correctly. - """ - # register get_endpoint mock data - register_api_route_fixture_file( - "transfer", - f"/endpoint/{GO_EP1_ID}/autoactivate", - "activation_stub.json", - method="POST", - ) - - # load and check the activation doc - with pytest.warns(globus_sdk.exc.RemovedInV4Warning): - res = client.endpoint_autoactivate(GO_EP1_ID) - assert res["code"] == "AutoActivated.CachedCredential" - - # check the formatted url for the request - req = get_last_request() - assert ( - req.url - == f"https://transfer.api.globus.org/v0.10/endpoint/{GO_EP1_ID}/autoactivate" - ) - - register_api_route_fixture_file( - "transfer", - f"/endpoint/{GO_EP1_ID}/autoactivate", - "activation_already_activated_stub.json", - method="POST", - replace=True, - ) - with pytest.warns(globus_sdk.exc.RemovedInV4Warning): - res = client.endpoint_autoactivate(GO_EP1_ID, if_expires_in=300) - assert res["code"] == "AlreadyActivated" - - req = get_last_request() - parsed_qs = urllib.parse.parse_qs(urllib.parse.urlparse(req.url).query) - assert parsed_qs == {"if_expires_in": ["300"]} diff --git a/tests/unit/responses/test_activation_response.py b/tests/unit/responses/test_activation_response.py deleted file mode 100644 index 9beb5fa00..000000000 --- a/tests/unit/responses/test_activation_response.py +++ /dev/null @@ -1,117 +0,0 @@ -import json -import time -from unittest import mock - -import pytest -import requests - -from globus_sdk.response import GlobusHTTPResponse -from globus_sdk.services.transfer.response import ActivationRequirementsResponse - - -def make_response( - activated=True, - expires_in=0, - auto_activation_supported=True, - oauth_server=None, - DATA=None, -): - """ - Helper for making ActivationRequirementsResponses with known fields - """ - DATA = DATA or [] - data = { - "activated": activated, - "expires_in": expires_in, - "oauth_server": oauth_server, - "DATA": DATA, - "auto_activation_supported": auto_activation_supported, - } - response = requests.Response() - response.headers["Content-Type"] = "application/json" - response._content = json.dumps(data).encode("utf-8") - return ActivationRequirementsResponse( - GlobusHTTPResponse(response, client=mock.Mock()) - ) - - -def test_expires_at(): - """ - Confirms expires_at is set properly by __init__ - """ - for seconds in [0, 10, 100, 1000, -10]: - response = make_response(expires_in=seconds) - expected = int(time.time()) + seconds - # make sure within a 1 second range of expected value - assert response.expires_at in (expected - 1, expected, expected + 1) - - # -1 marks no expiration - response = make_response(expires_in=-1) - assert response.expires_at is None - - -@pytest.mark.parametrize("value", (True, False)) -def test_supports_auto_activation(value): - """ - Gets supports_auto_activation from made responses, validates results - """ - response = make_response(auto_activation_supported=value) - assert response.supports_auto_activation == value - - -def test_supports_web_activation(): - """ - Gets supports_web_activation from made responses, validates results - """ - # true if auto_activatable, - response = make_response(auto_activation_supported=True) - assert response.supports_web_activation - # has an oauth server, - response = make_response(auto_activation_supported=False, oauth_server="server") - assert response.supports_web_activation - # or one of the other documents is myproxy or delegate_myproxy, - response = make_response( - auto_activation_supported=False, DATA=[{"type": "myproxy"}] - ) - assert response.supports_web_activation - response = make_response( - auto_activation_supported=False, DATA=[{"type": "delegate_myproxy"}] - ) - assert response.supports_web_activation - - # otherwise false - response = make_response(auto_activation_supported=False) - assert not response.supports_web_activation - - -def test_active_until(): - """ - Calls active_until on made responses, validates results - """ - # not active at all - response = make_response(activated=False) - assert not response.active_until(0) - - # always active - response = make_response(expires_in=-1) - assert response.active_until(0) - - response = make_response(expires_in=10) - # relative time - assert response.active_until(5) - assert not response.active_until(15) - # absolute time - now = int(time.time()) - assert response.active_until(now + 5, relative_time=False) - assert not response.active_until(now + 15, relative_time=False) - - -def test_always_activated(): - """ - Gets always_activated property from made responses, validates results - """ - response = make_response(expires_in=-1) - assert response.always_activated - - response = make_response(expires_in=0) - assert not response.always_activated From d53e0635fb7e3fb26822fef03f7955559f31a651 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 23 Jul 2025 21:07:05 +0000 Subject: [PATCH 124/176] (actions) update PR references --- changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst b/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst index f41fc9490..2e9f8486d 100644 --- a/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst +++ b/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst @@ -2,7 +2,7 @@ Removed ------- - Removed support for Endpoint Activation, a feature which was specific to - Globus Connect Server v4. (:pr:`NUMBER`) + Globus Connect Server v4. (:pr:`1279`) - Removed the activation methods: ``TransferClient.endpoint_autoactivate``, ``TransferClient.endpoint_activate``, From b193defc51b183c407d4900e49dc50049306f9c9 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 23 Jul 2025 16:15:49 -0500 Subject: [PATCH 125/176] Remove the ComputeClient alias (#1282) --- ...3_155826_sirosen_remove_compute_client.rst | 6 ++++ docs/services/compute.rst | 10 ------ src/globus_sdk/__init__.pyi | 2 -- .../services/compute/deprecated_client.py | 32 ------------------- .../compute/test_deprecated_client_alias.py | 27 ---------------- tests/unit/test_lazy_imports.py | 1 - .../unit/test_paginator_signature_matching.py | 15 ++++----- 7 files changed, 12 insertions(+), 81 deletions(-) create mode 100644 changelog.d/20250723_155826_sirosen_remove_compute_client.rst delete mode 100644 src/globus_sdk/services/compute/deprecated_client.py delete mode 100644 tests/unit/services/compute/test_deprecated_client_alias.py diff --git a/changelog.d/20250723_155826_sirosen_remove_compute_client.rst b/changelog.d/20250723_155826_sirosen_remove_compute_client.rst new file mode 100644 index 000000000..dcfbf9291 --- /dev/null +++ b/changelog.d/20250723_155826_sirosen_remove_compute_client.rst @@ -0,0 +1,6 @@ +Removed +------- + +- Removed the ``ComputeClient`` alias. This name was deprecated in + ``globus-sdk`` version 3. Users should use ``ComputeClientV2`` or + ``ComputeClientV3`` instead. (:pr:`NUMBER`) diff --git a/docs/services/compute.rst b/docs/services/compute.rst index 6f022a904..522ed0ffb 100644 --- a/docs/services/compute.rst +++ b/docs/services/compute.rst @@ -37,16 +37,6 @@ supports the latest API features and improvements. .. listknownscopes:: globus_sdk.scopes.ComputeScopes :base_name: ComputeClientV3.scopes - -.. py:class:: ComputeClient - - A deprecated alias for :class:`ComputeClientV2`. - - .. warning:: - - This class will be removed in ``globus-sdk`` version 4. - Users should migrate to one of the explicitly-versioned classes. - Client Errors ------------- diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index a74e41c76..892ba4b89 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -51,7 +51,6 @@ from .services.compute import ( ComputeFunctionDocument, ComputeFunctionMetadata, ) -from .services.compute.deprecated_client import ComputeClient from .services.flows import ( FlowsAPIError, FlowsClient, @@ -176,7 +175,6 @@ __all__ = ( "OAuthTokenResponse", "IDTokenDecoder", "ComputeAPIError", - "ComputeClient", "ComputeClientV2", "ComputeClientV3", "ComputeFunctionDocument", diff --git a/src/globus_sdk/services/compute/deprecated_client.py b/src/globus_sdk/services/compute/deprecated_client.py deleted file mode 100644 index 9c2d6f7ad..000000000 --- a/src/globus_sdk/services/compute/deprecated_client.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import sys -import typing as t - -from globus_sdk.exc import warn_deprecated - -from .client import ComputeClientV2 - -__all__ = ("ComputeClient",) - -if t.TYPE_CHECKING: - - class ComputeClient(ComputeClientV2): - pass - -else: - - def __getattr__(name: str) -> t.Any: - if name == "ComputeClient": - warn_deprecated( - "'globus_sdk.ComputeClient' is deprecated and will be removed " - "in the future. Prefer 'globus_sdk.ComputeClientV2'." - ) - - class ComputeClient(ComputeClientV2): - """A deprecated alias for 'globus_sdk.ComputeClientV2'.""" - - setattr(sys.modules[__name__], name, ComputeClient) - return ComputeClient - - raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/tests/unit/services/compute/test_deprecated_client_alias.py b/tests/unit/services/compute/test_deprecated_client_alias.py deleted file mode 100644 index 56a3e4995..000000000 --- a/tests/unit/services/compute/test_deprecated_client_alias.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest - -import globus_sdk -from globus_sdk import ComputeClientV2, RemovedInV4Warning - - -def test_legacy_client_warns_on_import(): - from globus_sdk.services.compute import ( - deprecated_client as deprecated_client_module, - ) - - # first, remove the object from the module's `__dict__` if it was there - # ensures that access will run `__getattr__` - if "ComputeClient" in deprecated_client_module.__dict__: - del deprecated_client_module.__dict__["ComputeClient"] - # and, similarly, remove it from 'globus_sdk' - if "ComputeClient" in globus_sdk.__dict__: - del globus_sdk.__dict__["ComputeClient"] - - with pytest.warns(RemovedInV4Warning, match="deprecated"): - from globus_sdk import ComputeClient # noqa: F401 - - -@pytest.mark.filterwarnings("ignore::globus_sdk.RemovedInV4Warning") -def test_legacy_client_is_v2(): - client = globus_sdk.ComputeClient() - assert isinstance(client, ComputeClientV2) diff --git a/tests/unit/test_lazy_imports.py b/tests/unit/test_lazy_imports.py index a9564c74f..ec3ddf83e 100644 --- a/tests/unit/test_lazy_imports.py +++ b/tests/unit/test_lazy_imports.py @@ -8,7 +8,6 @@ def test_explicit_dir_func_works(): assert "__all__" in dir(globus_sdk) -@pytest.mark.filterwarnings("ignore::globus_sdk.RemovedInV4Warning") def test_force_eager_imports_can_run(): # this check will not do much, other than ensuring that this does not crash globus_sdk._force_eager_imports() diff --git a/tests/unit/test_paginator_signature_matching.py b/tests/unit/test_paginator_signature_matching.py index e95d10025..2b1d7e8cf 100644 --- a/tests/unit/test_paginator_signature_matching.py +++ b/tests/unit/test_paginator_signature_matching.py @@ -4,21 +4,18 @@ """ import inspect -import warnings import pytest import globus_sdk _CLIENTS_TO_CHECK = [] -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=globus_sdk.RemovedInV4Warning) - for attrname in dir(globus_sdk): - obj = getattr(globus_sdk, attrname) - if obj is globus_sdk.BaseClient: - continue - if isinstance(obj, type) and issubclass(obj, globus_sdk.BaseClient): - _CLIENTS_TO_CHECK.append(obj) +for attrname in dir(globus_sdk): + obj = getattr(globus_sdk, attrname) + if obj is globus_sdk.BaseClient: + continue + if isinstance(obj, type) and issubclass(obj, globus_sdk.BaseClient): + _CLIENTS_TO_CHECK.append(obj) _METHODS_TO_CHECK = [] for cls in _CLIENTS_TO_CHECK: From efa4b10ad3da061a27b3672d58a0717390aef351 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 23 Jul 2025 21:16:02 +0000 Subject: [PATCH 126/176] (actions) update PR references --- changelog.d/20250723_155826_sirosen_remove_compute_client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250723_155826_sirosen_remove_compute_client.rst b/changelog.d/20250723_155826_sirosen_remove_compute_client.rst index dcfbf9291..f4cce3f1e 100644 --- a/changelog.d/20250723_155826_sirosen_remove_compute_client.rst +++ b/changelog.d/20250723_155826_sirosen_remove_compute_client.rst @@ -3,4 +3,4 @@ Removed - Removed the ``ComputeClient`` alias. This name was deprecated in ``globus-sdk`` version 3. Users should use ``ComputeClientV2`` or - ``ComputeClientV3`` instead. (:pr:`NUMBER`) + ``ComputeClientV3`` instead. (:pr:`1282`) From 5041fc0960cc68479ee768710fa7e0f8877c0016 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 23 Jul 2025 17:42:56 -0500 Subject: [PATCH 127/176] Remove the "write" endpoint server methods (#1284) --- ...sirosen_remove_endpoint_server_methods.rst | 8 +++ src/globus_sdk/services/transfer/client.py | 69 ------------------- 2 files changed, 8 insertions(+), 69 deletions(-) create mode 100644 changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst diff --git a/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst b/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst new file mode 100644 index 000000000..6beeb0009 --- /dev/null +++ b/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst @@ -0,0 +1,8 @@ +Removed +------- + +- Removed ``TransferClient`` methods for modifying "endpoint servers", a + feature specific to Globus Connect Server v4. Specifically, + ``add_endpoint_server``, ``update_endpoint_server``, and + ``delete_endpoint_server``. + These methods were deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index cf79d03ff..1f491ed69 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -736,75 +736,6 @@ def get_endpoint_server( query_params=query_params, ) - def add_endpoint_server( - self, endpoint_id: uuid.UUID | str, server_data: dict[str, t.Any] - ) -> response.GlobusHTTPResponse: - """ - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param endpoint_id: The endpoint under which the server is being registered - :param server_data: Fields for the new server, as a server document - """ - exc.warn_deprecated( - "add_endpoint_server is specific to Globus Connect Server v4, " - "which is no longer supported by the Transfer API." - ) - log.debug(f"TransferClient.add_endpoint_server({endpoint_id}, ...)") - return self.post(f"/v0.10/endpoint/{endpoint_id}/server", data=server_data) - - def update_endpoint_server( - self, - endpoint_id: uuid.UUID | str, - server_id: IntLike, - server_data: dict[str, t.Any], - ) -> response.GlobusHTTPResponse: - """ - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param endpoint_id: The endpoint under which the server is registered - :param server_id: The ID of the server to update - :param server_data: Fields on the server to update, as a partial server document - """ - exc.warn_deprecated( - "update_endpoint_server is specific to Globus Connect Server v4, " - "which is no longer supported by the Transfer API." - ) - log.debug( - "TransferClient.update_endpoint_server(%s, %s, ...)", - endpoint_id, - server_id, - ) - return self.put( - f"/v0.10/endpoint/{endpoint_id}/server/{server_id}", data=server_data - ) - - def delete_endpoint_server( - self, endpoint_id: uuid.UUID | str, server_id: IntLike - ) -> response.GlobusHTTPResponse: - """ - .. warning:: - - This method is deprecated with the end of Globus Connect Server v4 - support and may no longer function with the Transfer API. - - :param endpoint_id: The endpoint under which the server is registered - :param server_id: The ID of the server to delete - """ - exc.warn_deprecated( - "delete_endpoint_server is specific to Globus Connect Server v4, " - "which is no longer supported by the Transfer API." - ) - log.debug( - "TransferClient.delete_endpoint_server(%s, %s)", endpoint_id, server_id - ) - return self.delete(f"/v0.10/endpoint/{endpoint_id}/server/{server_id}") - # # Roles # From daf5f6e7ad8fce12246ccd4d573411de5545b27b Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 23 Jul 2025 17:43:09 -0500 Subject: [PATCH 128/176] Remove 'raw_text' (deprecated) (#1283) --- .../20250723_161421_sirosen_rm_raw_text.rst | 6 ++++++ src/globus_sdk/exc/api.py | 12 ------------ tests/unit/errors/test_common_functionality.py | 15 +-------------- 3 files changed, 7 insertions(+), 26 deletions(-) create mode 100644 changelog.d/20250723_161421_sirosen_rm_raw_text.rst diff --git a/changelog.d/20250723_161421_sirosen_rm_raw_text.rst b/changelog.d/20250723_161421_sirosen_rm_raw_text.rst new file mode 100644 index 000000000..a8ee71fd0 --- /dev/null +++ b/changelog.d/20250723_161421_sirosen_rm_raw_text.rst @@ -0,0 +1,6 @@ +Removed +------- + +- Removed ``GlobusAPIError.raw_text``. This attribute was deprecated in + ``globus-sdk`` version 3. Users should use the ``text`` attribute instead. + (:pr:`NUMBER`) diff --git a/src/globus_sdk/exc/api.py b/src/globus_sdk/exc/api.py index 248ce5292..fcf29b109 100644 --- a/src/globus_sdk/exc/api.py +++ b/src/globus_sdk/exc/api.py @@ -8,7 +8,6 @@ from .base import GlobusError from .err_info import ErrorInfoContainer -from .warnings import warn_deprecated if t.TYPE_CHECKING: import requests @@ -151,17 +150,6 @@ def text(self) -> str: """ return self._underlying_response.text - @property - def raw_text(self) -> str: - """ - Deprecated alias of the ``text`` property. - """ - warn_deprecated( - "The 'raw_text' property of GlobusAPIError objects is deprecated. " - "Use the 'text' property instead." - ) - return self.text - @property def binary_content(self) -> bytes: """ diff --git a/tests/unit/errors/test_common_functionality.py b/tests/unit/errors/test_common_functionality.py index dff25e365..b69fa9176 100644 --- a/tests/unit/errors/test_common_functionality.py +++ b/tests/unit/errors/test_common_functionality.py @@ -3,7 +3,7 @@ import pytest import requests -from globus_sdk import ErrorSubdocument, GlobusAPIError, RemovedInV4Warning, exc +from globus_sdk import ErrorSubdocument, GlobusAPIError, exc from globus_sdk.testing import construct_error @@ -52,19 +52,6 @@ def test_binary_content_property(): assert err.binary_content == body_text.encode("utf-8") -def test_raw_text_property_warns(): - body_text = "some data" - err = construct_error(body=body_text, http_status=400) - with pytest.warns( - RemovedInV4Warning, - match=( - r"The 'raw_text' property of GlobusAPIError objects is deprecated\. " - r"Use the 'text' property instead\." - ), - ): - assert err.raw_text == body_text - - @pytest.mark.parametrize( "body, response_headers, http_status, expect_code, expect_message", ( From f6b47c267fc6b9bf2939294a42857fdfc675d5e5 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 23 Jul 2025 22:43:20 +0000 Subject: [PATCH 129/176] (actions) update PR references --- changelog.d/20250723_161421_sirosen_rm_raw_text.rst | 2 +- .../20250723_162129_sirosen_remove_endpoint_server_methods.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/20250723_161421_sirosen_rm_raw_text.rst b/changelog.d/20250723_161421_sirosen_rm_raw_text.rst index a8ee71fd0..dac479151 100644 --- a/changelog.d/20250723_161421_sirosen_rm_raw_text.rst +++ b/changelog.d/20250723_161421_sirosen_rm_raw_text.rst @@ -3,4 +3,4 @@ Removed - Removed ``GlobusAPIError.raw_text``. This attribute was deprecated in ``globus-sdk`` version 3. Users should use the ``text`` attribute instead. - (:pr:`NUMBER`) + (:pr:`1283`) diff --git a/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst b/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst index 6beeb0009..7335d67d3 100644 --- a/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst +++ b/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst @@ -5,4 +5,4 @@ Removed feature specific to Globus Connect Server v4. Specifically, ``add_endpoint_server``, ``update_endpoint_server``, and ``delete_endpoint_server``. - These methods were deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) + These methods were deprecated in ``globus-sdk`` version 3. (:pr:`1284`) From dee0ca69e41fc364b784b2194a7a6e852372c6c3 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 24 Jul 2025 10:41:26 -0500 Subject: [PATCH 130/176] Remove 'operation_symlink' (deprecated) (#1286) --- ...74602_sirosen_remove_operation_symlink.rst | 5 +++ src/globus_sdk/services/transfer/client.py | 38 ------------------- .../transfer/test_operation_symlink.py | 29 -------------- 3 files changed, 5 insertions(+), 67 deletions(-) create mode 100644 changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst delete mode 100644 tests/functional/services/transfer/test_operation_symlink.py diff --git a/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst b/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst new file mode 100644 index 000000000..b142d866a --- /dev/null +++ b/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst @@ -0,0 +1,5 @@ +Removed +------- + +- Removed ``TransferClient.operation_symlink``. This method was deprecated in + ``globus-sdk`` version 3. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index 1f491ed69..edc8b11a6 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -1336,44 +1336,6 @@ def operation_stat( f"/v0.10/operation/endpoint/{endpoint_id}/stat", query_params=query_params ) - def operation_symlink( - self, - endpoint_id: uuid.UUID | str, - symlink_target: str, - path: str, - *, - query_params: dict[str, t.Any] | None = None, - ) -> response.GlobusHTTPResponse: - """ - :param endpoint_id: The ID of the endpoint on which to create a symlink - :param symlink_target: The path referenced by the new symlink - :param path: The name of (path to) the new symlink - :param query_params: Additional passthrough query parameters - - .. warning:: - - This method is not currently supported by any collections. - """ # noqa: E501 - exc.warn_deprecated( - "operation_symlink is not currently supported by any collections. " - "To reduce confusion, this method will be removed." - ) - log.debug( - "TransferClient.operation_symlink({}, {}, {}, {})".format( - endpoint_id, symlink_target, path, query_params - ) - ) - data = { - "DATA_TYPE": "symlink", - "symlink_target": symlink_target, - "path": path, - } - return self.post( - f"/v0.10/operation/endpoint/{endpoint_id}/symlink", - data=data, - query_params=query_params, - ) - # # Task Submission # diff --git a/tests/functional/services/transfer/test_operation_symlink.py b/tests/functional/services/transfer/test_operation_symlink.py deleted file mode 100644 index 6cda727fa..000000000 --- a/tests/functional/services/transfer/test_operation_symlink.py +++ /dev/null @@ -1,29 +0,0 @@ -import uuid - -import pytest - -from globus_sdk import exc -from globus_sdk.testing import RegisteredResponse - - -@pytest.fixture -def symlink_endpoint_id(): - return str(uuid.uuid1()) - - -@pytest.fixture(autouse=True) -def _setup_symlink_response(symlink_endpoint_id): - RegisteredResponse( - service="transfer", - method="POST", - path=f"/v0.10/operation/endpoint/{symlink_endpoint_id}/symlink", - json={}, - ).add() - - -def test_operation_symlink_warns(client, symlink_endpoint_id): - with pytest.warns( - exc.RemovedInV4Warning, - match="operation_symlink is not currently supported by any collections", - ): - client.operation_symlink(symlink_endpoint_id, "some_link_target", "/some/path") From 77eb6ad8536ee135c627dbc96b9bf6e206405c8a Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 24 Jul 2025 15:41:38 +0000 Subject: [PATCH 131/176] (actions) update PR references --- .../20250723_174602_sirosen_remove_operation_symlink.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst b/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst index b142d866a..c7ae1f88a 100644 --- a/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst +++ b/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst @@ -2,4 +2,4 @@ Removed ------- - Removed ``TransferClient.operation_symlink``. This method was deprecated in - ``globus-sdk`` version 3. (:pr:`NUMBER`) + ``globus-sdk`` version 3. (:pr:`1286`) From 7e4b64be9b350c910e16eebd6d5cf9be47ab02b5 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 24 Jul 2025 10:42:05 -0500 Subject: [PATCH 132/176] Remove deprecated Compute data helpers (#1285) --- ...723_162859_sirosen_remove_compute_data.rst | 6 ++ scripts/ensure_exports_are_documented.py | 5 +- src/globus_sdk/__init__.pyi | 4 -- src/globus_sdk/services/compute/__init__.py | 3 - src/globus_sdk/services/compute/data.py | 67 ------------------- .../services/compute/test_deprecated_data.py | 21 ------ 6 files changed, 7 insertions(+), 99 deletions(-) create mode 100644 changelog.d/20250723_162859_sirosen_remove_compute_data.rst delete mode 100644 src/globus_sdk/services/compute/data.py delete mode 100644 tests/unit/services/compute/test_deprecated_data.py diff --git a/changelog.d/20250723_162859_sirosen_remove_compute_data.rst b/changelog.d/20250723_162859_sirosen_remove_compute_data.rst new file mode 100644 index 000000000..4c428c7e7 --- /dev/null +++ b/changelog.d/20250723_162859_sirosen_remove_compute_data.rst @@ -0,0 +1,6 @@ +Removed +------- + +- Removed the ``ComputeFunctionDocument`` and ``ComputeFunctionMetadata`` + classes. These helpers were deprecated in ``globus-sdk`` version 3. + (:pr:`NUMBER`) diff --git a/scripts/ensure_exports_are_documented.py b/scripts/ensure_exports_are_documented.py index 5eba1fa92..9d8108e78 100755 --- a/scripts/ensure_exports_are_documented.py +++ b/scripts/ensure_exports_are_documented.py @@ -23,10 +23,7 @@ "globus_sdk/testing/", ) -DEPRECATED_NAMES = { - "ComputeFunctionDocument", - "ComputeFunctionMetadata", -} +DEPRECATED_NAMES: set[str] = set() def load_docs() -> dict[str, str]: diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index 892ba4b89..8c40b762c 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -48,8 +48,6 @@ from .services.compute import ( ComputeAPIError, ComputeClientV2, ComputeClientV3, - ComputeFunctionDocument, - ComputeFunctionMetadata, ) from .services.flows import ( FlowsAPIError, @@ -177,8 +175,6 @@ __all__ = ( "ComputeAPIError", "ComputeClientV2", "ComputeClientV3", - "ComputeFunctionDocument", - "ComputeFunctionMetadata", "FlowsAPIError", "FlowsClient", "IterableFlowsResponse", diff --git a/src/globus_sdk/services/compute/__init__.py b/src/globus_sdk/services/compute/__init__.py index dd36e7d59..f8fc864af 100644 --- a/src/globus_sdk/services/compute/__init__.py +++ b/src/globus_sdk/services/compute/__init__.py @@ -1,11 +1,8 @@ from .client import ComputeClientV2, ComputeClientV3 -from .data import ComputeFunctionDocument, ComputeFunctionMetadata from .errors import ComputeAPIError __all__ = ( "ComputeAPIError", "ComputeClientV2", "ComputeClientV3", - "ComputeFunctionDocument", - "ComputeFunctionMetadata", ) diff --git a/src/globus_sdk/services/compute/data.py b/src/globus_sdk/services/compute/data.py deleted file mode 100644 index 4833a26df..000000000 --- a/src/globus_sdk/services/compute/data.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import annotations - -import uuid - -from globus_sdk._missing import MISSING, MissingType -from globus_sdk._payload import GlobusPayload -from globus_sdk.exc import warn_deprecated - - -class ComputeFunctionMetadata(GlobusPayload): - """ - .. warning:: - - This class is deprecated. - - A wrapper for function metadata. - - :param python_version: The Python version used to serialize the function. - :param sdk_version: The Globus Compute SDK version used to serialize the function. - """ - - def __init__( - self, - *, - python_version: str | MissingType = MISSING, - sdk_version: str | MissingType = MISSING, - ) -> None: - warn_deprecated("ComputeFunctionMetadata is deprecated.") - super().__init__() - self["python_version"] = python_version - self["sdk_version"] = sdk_version - - -class ComputeFunctionDocument(GlobusPayload): - """ - .. warning:: - - This class is deprecated. - - A function registration document. - - :param function_name: The name of the function. - :param function_code: The serialized function source code. - :param description: The description of the function. - :param metadata: The metadata of the function. - :param group: Restrict function access to members of this Globus group. - :param public: Indicates whether the function is public. - """ - - def __init__( - self, - *, - function_name: str, - function_code: str, - description: str | MissingType = MISSING, - metadata: ComputeFunctionMetadata | MissingType = MISSING, - group: uuid.UUID | str | MissingType = MISSING, - public: bool = False, - ) -> None: - warn_deprecated("ComputeFunctionDocument is deprecated.") - super().__init__() - self["function_name"] = function_name - self["function_code"] = function_code - self["description"] = description - self["metadata"] = metadata - self["group"] = group - self["public"] = public diff --git a/tests/unit/services/compute/test_deprecated_data.py b/tests/unit/services/compute/test_deprecated_data.py deleted file mode 100644 index 16ed94b87..000000000 --- a/tests/unit/services/compute/test_deprecated_data.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest - -from globus_sdk.exc import RemovedInV4Warning -from globus_sdk.services.compute.data import ( - ComputeFunctionDocument, - ComputeFunctionMetadata, -) - - -def test_compute_function_metadata_deprecated(): - with pytest.warns( - RemovedInV4Warning, match="ComputeFunctionMetadata is deprecated." - ): - ComputeFunctionMetadata() - - -def test_compute_function_document_deprecated(): - with pytest.warns( - RemovedInV4Warning, match="ComputeFunctionDocument is deprecated." - ): - ComputeFunctionDocument(function_name="foo", function_code="bar") From 82cce06516e7cea65dd1ce376029f00d491ff05f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 24 Jul 2025 15:42:18 +0000 Subject: [PATCH 133/176] (actions) update PR references --- changelog.d/20250723_162859_sirosen_remove_compute_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250723_162859_sirosen_remove_compute_data.rst b/changelog.d/20250723_162859_sirosen_remove_compute_data.rst index 4c428c7e7..9964d1ba1 100644 --- a/changelog.d/20250723_162859_sirosen_remove_compute_data.rst +++ b/changelog.d/20250723_162859_sirosen_remove_compute_data.rst @@ -3,4 +3,4 @@ Removed - Removed the ``ComputeFunctionDocument`` and ``ComputeFunctionMetadata`` classes. These helpers were deprecated in ``globus-sdk`` version 3. - (:pr:`NUMBER`) + From e564f1903676dda70ee48636a7d1ec5785674b59 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 25 Jul 2025 16:01:23 -0500 Subject: [PATCH 134/176] Bump version and changelog for release --- ...829_max.tuecke_sc_26339_rename_testing.rst | 4 - ...ax.tuecke_sc_35529_rename_tokenstorage.rst | 4 - ...sc_41884_replace_scope_collection_type.rst | 4 - ...09_8730430+m1yag1_sc_43037_caller_info.rst | 4 - ...18_155001_sirosen_rename_function_data.rst | 5 - ...5657_sirosen_remove_from_transfer_data.rst | 6 -- ...8_162517_sirosen_remove_validate_token.rst | 6 -- ...n_auth_service_client_remove_client_id.rst | 5 - ..._180517_sirosen_remove_oauth2_userinfo.rst | 5 - ...81610_sirosen_remove_cc_get_identities.rst | 13 --- ...22_150116_sirosen_remove_gcsv4_methods.rst | 6 -- .../20250722_162951_sirosen_4_x_dev.rst | 6 -- ...23_110332_sirosen_remove_gcsv4_methods.rst | 17 ---- ...3_155826_sirosen_remove_compute_client.rst | 6 -- .../20250723_161421_sirosen_rm_raw_text.rst | 6 -- ...sirosen_remove_endpoint_server_methods.rst | 8 -- ...723_162859_sirosen_remove_compute_data.rst | 6 -- ...74602_sirosen_remove_operation_symlink.rst | 5 - changelog.rst | 96 +++++++++++++++++++ pyproject.toml | 2 +- 20 files changed, 97 insertions(+), 117 deletions(-) delete mode 100644 changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst delete mode 100644 changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst delete mode 100644 changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst delete mode 100644 changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst delete mode 100644 changelog.d/20250718_155001_sirosen_rename_function_data.rst delete mode 100644 changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst delete mode 100644 changelog.d/20250718_162517_sirosen_remove_validate_token.rst delete mode 100644 changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst delete mode 100644 changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst delete mode 100644 changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst delete mode 100644 changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst delete mode 100644 changelog.d/20250722_162951_sirosen_4_x_dev.rst delete mode 100644 changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst delete mode 100644 changelog.d/20250723_155826_sirosen_remove_compute_client.rst delete mode 100644 changelog.d/20250723_161421_sirosen_rm_raw_text.rst delete mode 100644 changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst delete mode 100644 changelog.d/20250723_162859_sirosen_remove_compute_data.rst delete mode 100644 changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst diff --git a/changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst b/changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst deleted file mode 100644 index 63d4be747..000000000 --- a/changelog.d/20250710_115829_max.tuecke_sc_26339_rename_testing.rst +++ /dev/null @@ -1,4 +0,0 @@ -Changed -------- - -- Renamed the ``globus_sdk._testing`` subpackage to ``globus_sdk.testing``. (:pr:`1251`) diff --git a/changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst b/changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst deleted file mode 100644 index c5a666568..000000000 --- a/changelog.d/20250710_124314_max.tuecke_sc_35529_rename_tokenstorage.rst +++ /dev/null @@ -1,4 +0,0 @@ -Changed -------- - -- Renamed the ``globus_sdk.tokenstorage`` subpackage to ``globus_sdk.token_storage`` and removed the ``globus_sdk.experimental.tokenstorage`` (:pr:`1252`) diff --git a/changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst b/changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst deleted file mode 100644 index 175cc6b1c..000000000 --- a/changelog.d/20250715_141115_max.tuecke_sc_41884_replace_scope_collection_type.rst +++ /dev/null @@ -1,4 +0,0 @@ -Changed -------- - -- Remove support for normalizing nested iterables of scopes, e.g. ``[["scope1"], "scope2"]`` (:pr:`1259`) diff --git a/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst b/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst deleted file mode 100644 index ba29f1210..000000000 --- a/changelog.d/20250716_105609_8730430+m1yag1_sc_43037_caller_info.rst +++ /dev/null @@ -1,4 +0,0 @@ -Added ------ - -- Add ``RequestCallerInfo`` data object to ``RequestsTransport.request`` for passing caller context information. (:pr:`1261`) diff --git a/changelog.d/20250718_155001_sirosen_rename_function_data.rst b/changelog.d/20250718_155001_sirosen_rename_function_data.rst deleted file mode 100644 index de3806e9b..000000000 --- a/changelog.d/20250718_155001_sirosen_rename_function_data.rst +++ /dev/null @@ -1,5 +0,0 @@ -Breaking Changes ----------------- - -- The ``function_data`` argument to ``ComputeClientV2.register_function`` has - been renamed to ``data`` to be consistent with other usages. diff --git a/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst b/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst deleted file mode 100644 index 68dc1c7ec..000000000 --- a/changelog.d/20250718_155657_sirosen_remove_from_transfer_data.rst +++ /dev/null @@ -1,6 +0,0 @@ -Breaking Changes ----------------- - -- The ``TimerJob.from_transfer_data`` classmethod, which was deprecated in - globus-sdk version 3, has been removed. Users should use the ``TransferTimer`` - class to construct timers which submit transfer tasks. (:pr:`1269`) diff --git a/changelog.d/20250718_162517_sirosen_remove_validate_token.rst b/changelog.d/20250718_162517_sirosen_remove_validate_token.rst deleted file mode 100644 index d354cab95..000000000 --- a/changelog.d/20250718_162517_sirosen_remove_validate_token.rst +++ /dev/null @@ -1,6 +0,0 @@ -Breaking Changes ----------------- - -- The ``oauth2_validate_token`` method has been removed from - ``NativeAppAuthClient`` and ``ConfidentialAppAuthClient``. - This method was deprecated in globus-sdk v3. (:pr:`1270`) diff --git a/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst b/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst deleted file mode 100644 index 8f694bc65..000000000 --- a/changelog.d/20250718_175205_sirosen_auth_service_client_remove_client_id.rst +++ /dev/null @@ -1,5 +0,0 @@ -Breaking Changes ----------------- - -- ``AuthClient`` no longer accepts ``client_id`` as a parameter and does not - provide it as an attribute. This was deprecated in globus-sdk version 3. (:pr:`1271`) diff --git a/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst b/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst deleted file mode 100644 index 4f40f2c4f..000000000 --- a/changelog.d/20250718_180517_sirosen_remove_oauth2_userinfo.rst +++ /dev/null @@ -1,5 +0,0 @@ -Breaking Changes ----------------- - -- Removed ``AuthClient.oauth2_userinfo``. This method was deprecated in - ``globus-sdk`` version 3. (:pr:`1272`) diff --git a/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst b/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst deleted file mode 100644 index 62bf3d984..000000000 --- a/changelog.d/20250718_181610_sirosen_remove_cc_get_identities.rst +++ /dev/null @@ -1,13 +0,0 @@ -Breaking Changes ----------------- - -- Removed support for ``ConfidentialAppAuthClient.get_identities``. - This usage was deprecated in ``globus-sdk`` version 3. (:pr:`1273`) - - - Users calling the Get Identities API on behalf of a client identity should - instead get tokens for the client and use those tokens to call - ``AuthClient.get_identities``. For example, by instantiating an - ``AuthClient`` using a ``ClientCredentialsAuthorizer``. - - - This also means that it is no longer valid to use a - ``ConfidentialAppAuthClient`` to initialize an ``IdentityMap``. diff --git a/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst b/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst deleted file mode 100644 index aefc1bd17..000000000 --- a/changelog.d/20250722_150116_sirosen_remove_gcsv4_methods.rst +++ /dev/null @@ -1,6 +0,0 @@ -Removed -------- - -- ``TransferClient.create_endpoint`` has been removed. This method primarily - supported creation of GCSv4 servers and was deprecated in ``globus-sdk`` v3. - (:pr:`1276`) diff --git a/changelog.d/20250722_162951_sirosen_4_x_dev.rst b/changelog.d/20250722_162951_sirosen_4_x_dev.rst deleted file mode 100644 index b722ce03f..000000000 --- a/changelog.d/20250722_162951_sirosen_4_x_dev.rst +++ /dev/null @@ -1,6 +0,0 @@ -Removed -------- - -- ``GCSClient.connector_id_to_name()`` has been removed. It was deprecated in - ``globus-sdk`` version 3. Users should use ``globus_sdk.ConnectorTable`` - instead. (:pr:`1277`) diff --git a/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst b/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst deleted file mode 100644 index 2e9f8486d..000000000 --- a/changelog.d/20250723_110332_sirosen_remove_gcsv4_methods.rst +++ /dev/null @@ -1,17 +0,0 @@ -Removed -------- - -- Removed support for Endpoint Activation, a feature which was specific to - Globus Connect Server v4. (:pr:`1279`) - - - Removed the activation methods: ``TransferClient.endpoint_autoactivate``, - ``TransferClient.endpoint_activate``, - ``TransferClient.endpoint_deactivate``, and - ``TransferClient.endpoint_get_activation_requirements`` - - - Removed the specialized ``ActivationRequirementsResponse`` parsed response - type - - - ``TransferClient.update_endpoint`` would previously check the - ``myproxy_server`` and ``oauth_server`` parameters, which were solely used - for the purpose of configuring activation. It no longer does so. diff --git a/changelog.d/20250723_155826_sirosen_remove_compute_client.rst b/changelog.d/20250723_155826_sirosen_remove_compute_client.rst deleted file mode 100644 index f4cce3f1e..000000000 --- a/changelog.d/20250723_155826_sirosen_remove_compute_client.rst +++ /dev/null @@ -1,6 +0,0 @@ -Removed -------- - -- Removed the ``ComputeClient`` alias. This name was deprecated in - ``globus-sdk`` version 3. Users should use ``ComputeClientV2`` or - ``ComputeClientV3`` instead. (:pr:`1282`) diff --git a/changelog.d/20250723_161421_sirosen_rm_raw_text.rst b/changelog.d/20250723_161421_sirosen_rm_raw_text.rst deleted file mode 100644 index dac479151..000000000 --- a/changelog.d/20250723_161421_sirosen_rm_raw_text.rst +++ /dev/null @@ -1,6 +0,0 @@ -Removed -------- - -- Removed ``GlobusAPIError.raw_text``. This attribute was deprecated in - ``globus-sdk`` version 3. Users should use the ``text`` attribute instead. - (:pr:`1283`) diff --git a/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst b/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst deleted file mode 100644 index 7335d67d3..000000000 --- a/changelog.d/20250723_162129_sirosen_remove_endpoint_server_methods.rst +++ /dev/null @@ -1,8 +0,0 @@ -Removed -------- - -- Removed ``TransferClient`` methods for modifying "endpoint servers", a - feature specific to Globus Connect Server v4. Specifically, - ``add_endpoint_server``, ``update_endpoint_server``, and - ``delete_endpoint_server``. - These methods were deprecated in ``globus-sdk`` version 3. (:pr:`1284`) diff --git a/changelog.d/20250723_162859_sirosen_remove_compute_data.rst b/changelog.d/20250723_162859_sirosen_remove_compute_data.rst deleted file mode 100644 index 9964d1ba1..000000000 --- a/changelog.d/20250723_162859_sirosen_remove_compute_data.rst +++ /dev/null @@ -1,6 +0,0 @@ -Removed -------- - -- Removed the ``ComputeFunctionDocument`` and ``ComputeFunctionMetadata`` - classes. These helpers were deprecated in ``globus-sdk`` version 3. - diff --git a/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst b/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst deleted file mode 100644 index c7ae1f88a..000000000 --- a/changelog.d/20250723_174602_sirosen_remove_operation_symlink.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -------- - -- Removed ``TransferClient.operation_symlink``. This method was deprecated in - ``globus-sdk`` version 3. (:pr:`1286`) diff --git a/changelog.rst b/changelog.rst index a9f742bc5..5cb063104 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,102 @@ to a major new version of the SDK. .. scriv-insert-here +.. _changelog-4.0.0a4: + +v4.0.0a4 (2025-07-25) +===================== + +Breaking Changes +---------------- + +- The ``function_data`` argument to ``ComputeClientV2.register_function`` has + been renamed to ``data`` to be consistent with other usages. + +- ``AuthClient`` no longer accepts ``client_id`` as a parameter and does not + provide it as an attribute. This was deprecated in globus-sdk version 3. (:pr:`1271`) + +Added +----- + +- Add ``RequestCallerInfo`` data object to ``RequestsTransport.request`` for passing caller context information. (:pr:`1261`) + +Removed +------- + +- The ``TimerJob.from_transfer_data`` classmethod, which was deprecated in + globus-sdk version 3, has been removed. Users should use the ``TransferTimer`` + class to construct timers which submit transfer tasks. (:pr:`1269`) + +- The ``oauth2_validate_token`` method has been removed from + ``NativeAppAuthClient`` and ``ConfidentialAppAuthClient``. + This method was deprecated in globus-sdk v3. (:pr:`1270`) + +- Removed ``AuthClient.oauth2_userinfo``. This method was deprecated in + ``globus-sdk`` version 3. (:pr:`1272`) + +- Removed support for ``ConfidentialAppAuthClient.get_identities``. + This usage was deprecated in ``globus-sdk`` version 3. (:pr:`1273`) + + - Users calling the Get Identities API on behalf of a client identity should + instead get tokens for the client and use those tokens to call + ``AuthClient.get_identities``. For example, by instantiating an + ``AuthClient`` using a ``ClientCredentialsAuthorizer``. + + - This also means that it is no longer valid to use a + ``ConfidentialAppAuthClient`` to initialize an ``IdentityMap``. + +- ``TransferClient.create_endpoint`` has been removed. This method primarily + supported creation of GCSv4 servers and was deprecated in ``globus-sdk`` v3. + (:pr:`1276`) + +- ``GCSClient.connector_id_to_name()`` has been removed. It was deprecated in + ``globus-sdk`` version 3. Users should use ``globus_sdk.ConnectorTable`` + instead. (:pr:`1277`) + +- Removed support for Endpoint Activation, a feature which was specific to + Globus Connect Server v4. (:pr:`1279`) + + - Removed the activation methods: ``TransferClient.endpoint_autoactivate``, + ``TransferClient.endpoint_activate``, + ``TransferClient.endpoint_deactivate``, and + ``TransferClient.endpoint_get_activation_requirements`` + + - Removed the specialized ``ActivationRequirementsResponse`` parsed response + type + + - ``TransferClient.update_endpoint`` would previously check the + ``myproxy_server`` and ``oauth_server`` parameters, which were solely used + for the purpose of configuring activation. It no longer does so. + +- Removed the ``ComputeClient`` alias. This name was deprecated in + ``globus-sdk`` version 3. Users should use ``ComputeClientV2`` or + ``ComputeClientV3`` instead. (:pr:`1282`) + +- Removed ``GlobusAPIError.raw_text``. This attribute was deprecated in + ``globus-sdk`` version 3. Users should use the ``text`` attribute instead. + (:pr:`1283`) + +- Removed ``TransferClient`` methods for modifying "endpoint servers", a + feature specific to Globus Connect Server v4. Specifically, + ``add_endpoint_server``, ``update_endpoint_server``, and + ``delete_endpoint_server``. + These methods were deprecated in ``globus-sdk`` version 3. (:pr:`1284`) + +- Removed the ``ComputeFunctionDocument`` and ``ComputeFunctionMetadata`` + classes. These helpers were deprecated in ``globus-sdk`` version 3. + +- Removed ``TransferClient.operation_symlink``. This method was deprecated in + ``globus-sdk`` version 3. (:pr:`1286`) + +Changed +------- + +- Renamed the ``globus_sdk._testing`` subpackage to ``globus_sdk.testing``. (:pr:`1251`) + +- Renamed the ``globus_sdk.tokenstorage`` subpackage to ``globus_sdk.token_storage`` and removed the ``globus_sdk.experimental.tokenstorage`` (:pr:`1252`) + +- Remove support for normalizing nested iterables of scopes, e.g. ``[["scope1"], "scope2"]`` (:pr:`1259`) + .. _changelog-4.0.0a3: v4.0.0a3 (2025-07-10) diff --git a/pyproject.toml b/pyproject.toml index e093dc324..dd101de17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "globus-sdk" -version = "4.0.0a3" +version = "4.0.0a4" authors = [ { name = "Globus Team", email = "support@globus.org" }, ] From 7808e30dccfc41d53b675e96b34ea668a92ae6e4 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 23 Jul 2025 15:31:58 -0500 Subject: [PATCH 135/176] Convert 'scopes_to_str' to 'ScopeParser.serialize' This moves the function to the parser class, but also slightly simplifies its behavior. It now refrains from any "re-parsing" of data, and simply joins together strings provided by the user. Users who want to do more normalization have access to `ScopeParser.merge_scopes`, which does more rigorous handling than what was present in `scopes_to_str`. --- .../authorizers/client_credentials.py | 4 +- src/globus_sdk/scopes/_normalize.py | 42 +++---------------- src/globus_sdk/scopes/parser.py | 28 +++++++++++++ src/globus_sdk/services/auth/_common.py | 4 +- 4 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/globus_sdk/authorizers/client_credentials.py b/src/globus_sdk/authorizers/client_credentials.py index c9614b74c..4888d46bd 100644 --- a/src/globus_sdk/authorizers/client_credentials.py +++ b/src/globus_sdk/authorizers/client_credentials.py @@ -4,7 +4,7 @@ import typing as t import globus_sdk -from globus_sdk.scopes import Scope, scopes_to_str +from globus_sdk.scopes import Scope, ScopeParser from .renewing import RenewingAuthorizer @@ -67,7 +67,7 @@ def __init__( ) -> None: # values for _get_token_data self.confidential_client = confidential_client - self.scopes = scopes_to_str(scopes) + self.scopes = ScopeParser.serialize(scopes) log.debug( "Setting up ClientCredentialsAuthorizer with confidential_client=" diff --git a/src/globus_sdk/scopes/_normalize.py b/src/globus_sdk/scopes/_normalize.py index 2c9af3f57..f907ca3cb 100644 --- a/src/globus_sdk/scopes/_normalize.py +++ b/src/globus_sdk/scopes/_normalize.py @@ -6,27 +6,6 @@ from .representation import Scope -def scopes_to_str(scopes: str | Scope | t.Iterable[str | Scope]) -> str: - """ - Normalize scopes to a space-separated scope string. - - :param scopes: A scope string, scope object, or an iterable of scope strings - and scope objects. - :returns: A space-separated scope string. - - Example usage: - - .. code-block:: pycon - - >>> scopes_to_str(Scope("foo")) - 'foo' - >>> scopes_to_str(Scope("foo"), "bar", Scope("qux")) - 'foo bar qux' - """ - scope_iter = _iter_scope_collection(scopes, split_root_scopes=False) - return " ".join(str(scope) for scope in scope_iter) - - def scopes_to_scope_list(scopes: str | Scope | t.Iterable[str | Scope]) -> list[Scope]: """ Normalize scopes to a list of Scope objects. @@ -55,8 +34,6 @@ def scopes_to_scope_list(scopes: str | Scope | t.Iterable[str | Scope]) -> list[ def _iter_scope_collection( obj: str | Scope | t.Iterable[str | Scope], - *, - split_root_scopes: bool = True, ) -> t.Iterator[str | Scope]: """ Provide an iterator over a collection of scopes. @@ -66,10 +43,6 @@ def _iter_scope_collection( :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 - split. This flag allows a caller to optimize, skipping a bfs operation if - merging will be done later purely with strings. - Example usage: .. code-block:: pycon @@ -80,21 +53,18 @@ def _iter_scope_collection( [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]'] """ if isinstance(obj, str): - yield from _iter_scope_string(obj, split_root_scopes) + yield from _iter_scope_string(obj) elif isinstance(obj, Scope): yield obj else: - yield from _iter_scope_iterable(obj, split_root_scopes) + yield from _iter_scope_iterable(obj) -def _iter_scope_string(scope_str: str, split_root_scopes: bool) -> t.Iterator[str]: - if not split_root_scopes or " " not in scope_str: +def _iter_scope_string(scope_str: str) -> t.Iterator[str]: + if " " not in scope_str: yield scope_str - elif "[" not in scope_str: yield from scope_str.split(" ") else: @@ -103,11 +73,11 @@ def _iter_scope_string(scope_str: str, split_root_scopes: bool) -> t.Iterator[st def _iter_scope_iterable( - scope_iterable: t.Iterable[str | Scope], split_root_scopes: bool + scope_iterable: t.Iterable[str | Scope], ) -> t.Iterator[str | Scope]: for scope in scope_iterable: if isinstance(scope, str): - yield from _iter_scope_string(scope, split_root_scopes) + yield from _iter_scope_string(scope) elif isinstance(scope, Scope): yield scope else: diff --git a/src/globus_sdk/scopes/parser.py b/src/globus_sdk/scopes/parser.py index 90298c71e..c2e7b19d9 100644 --- a/src/globus_sdk/scopes/parser.py +++ b/src/globus_sdk/scopes/parser.py @@ -1,5 +1,7 @@ from __future__ import annotations +import typing as t + from ._graph_parser import ScopeGraph from .representation import Scope @@ -72,3 +74,29 @@ def merge_scopes(cls, scopes_a: list[Scope], scopes_b: list[Scope]) -> list[Scop return cls.parse( " ".join([str(s) for s in scopes_a] + [str(s) for s in scopes_b]) ) + + @classmethod + def serialize(cls, scopes: str | Scope | t.Iterable[str | Scope]) -> str: + """ + Normalize scopes to a space-separated scope string. + + The results of this method are suitable for sending to Globus Auth. + Scopes are not parsed, merged, or normalized by this method. + + :param scopes: A scope string, scope object, or an iterable of scope strings + and scope objects. + :returns: A space-separated scope string. + + Example usage: + + .. code-block:: pycon + + >>> ScopeParser.serialize([Scope("foo"), "bar", Scope("qux")]) + 'foo bar qux' + """ + scope_iter: t.Iterable[str | Scope] + if isinstance(scopes, (str, Scope)): + scope_iter = (scopes,) + else: + scope_iter = scopes + return " ".join(str(scope) for scope in scope_iter) diff --git a/src/globus_sdk/services/auth/_common.py b/src/globus_sdk/services/auth/_common.py index 55b5a33c7..aa83419de 100644 --- a/src/globus_sdk/services/auth/_common.py +++ b/src/globus_sdk/services/auth/_common.py @@ -10,7 +10,7 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk.exc import GlobusSDKUsageError from globus_sdk.response import GlobusHTTPResponse -from globus_sdk.scopes import Scope, scopes_to_str +from globus_sdk.scopes import Scope, ScopeParser log = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def stringify_requested_scopes( requested_scopes: str | Scope | t.Iterable[str | Scope], ) -> str: - requested_scopes_string: str = scopes_to_str(requested_scopes) + requested_scopes_string: str = ScopeParser.serialize(requested_scopes) if requested_scopes_string == "": raise GlobusSDKUsageError( "requested_scopes cannot be the empty string or empty collection" From f2afc52854b889e503b0b847c7aba39407e1327e Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 25 Jul 2025 17:03:44 -0500 Subject: [PATCH 136/176] Move 'scopes_to_scope_list' to private function This function was supported as a public interface but only had one known usage site, in `GlobusApp`. It has been removed and replaced with an internal method, `GlobusApp._iter_scopes`. Unit tests are updated to test the new replacement helper. Because the helper has a tighter contract in terms of what it supports and in what context, it is greatly simplified in the process as well. The older helper structure shows evidence of past iterations in terms of handling nested iterables. --- .../scopes_and_consents/scope_parsing.rst | 9 -- src/globus_sdk/globus_app/app.py | 21 +++- src/globus_sdk/scopes/__init__.py | 3 - src/globus_sdk/scopes/_normalize.py | 84 ------------- .../scope_collection_type.py | 4 +- .../globus_app/test_scope_normalization.py | 59 +++++++++ tests/unit/scopes/test_scope_normalization.py | 114 ------------------ tests/unit/scopes/test_scope_parser.py | 48 ++++++++ 8 files changed, 127 insertions(+), 215 deletions(-) delete mode 100644 src/globus_sdk/scopes/_normalize.py create mode 100644 tests/unit/globus_app/test_scope_normalization.py delete mode 100644 tests/unit/scopes/test_scope_normalization.py diff --git a/docs/authorization/scopes_and_consents/scope_parsing.rst b/docs/authorization/scopes_and_consents/scope_parsing.rst index 69710c085..1f0b80e85 100644 --- a/docs/authorization/scopes_and_consents/scope_parsing.rst +++ b/docs/authorization/scopes_and_consents/scope_parsing.rst @@ -24,12 +24,3 @@ ScopeParser Reference .. autoclass:: ScopeParseError .. autoclass:: ScopeCycleError - -.. rubric:: Utility Functions - -``globus_sdk.scopes`` also provides helper functions which are used to -manipulate scope objects. - -.. autofunction:: scopes_to_str - -.. autofunction:: scopes_to_scope_list diff --git a/src/globus_sdk/globus_app/app.py b/src/globus_sdk/globus_app/app.py index 408dbb0b8..ce6801cc2 100644 --- a/src/globus_sdk/globus_app/app.py +++ b/src/globus_sdk/globus_app/app.py @@ -14,7 +14,7 @@ ) from globus_sdk.authorizers import GlobusAuthorizer from globus_sdk.gare import GlobusAuthorizationParameters -from globus_sdk.scopes import AuthScopes, Scope, ScopeParser, scopes_to_scope_list +from globus_sdk.scopes import AuthScopes, Scope, ScopeParser from globus_sdk.token_storage import ( ScopeRequirementsValidator, TokenStorage, @@ -130,7 +130,7 @@ def _resolve_scope_requirements( return {} return { - resource_server: scopes_to_scope_list(scopes) + resource_server: list(self._iter_scopes(scopes)) for resource_server, scopes in scope_requirements.items() } @@ -424,7 +424,7 @@ def add_scope_requirements( """ for resource_server, scopes in scope_requirements.items(): curr = self._scope_requirements.setdefault(resource_server, []) - curr.extend(scopes_to_scope_list(scopes)) + curr.extend(self._iter_scopes(scopes)) self._authorizer_factory.clear_cache(*scope_requirements.keys()) @@ -462,3 +462,18 @@ def scope_requirements(self) -> dict[str, list[Scope]]: """ # Scopes are mutable objects so we return a deepcopy return copy.deepcopy(self._scope_requirements) + + def _iter_scopes( + self, scopes: str | Scope | t.Iterable[str | Scope] + ) -> t.Iterator[Scope]: + """Normalize scopes in various formats to an iterator of Scope objects.""" + if isinstance(scopes, str): + yield from ScopeParser.parse(scopes) + elif isinstance(scopes, Scope): + yield scopes + else: + for item in scopes: + if isinstance(item, str): + yield from ScopeParser.parse(item) + else: + yield item diff --git a/src/globus_sdk/scopes/__init__.py b/src/globus_sdk/scopes/__init__.py index 21b476771..8bd7ccacc 100644 --- a/src/globus_sdk/scopes/__init__.py +++ b/src/globus_sdk/scopes/__init__.py @@ -1,4 +1,3 @@ -from ._normalize import scopes_to_scope_list, scopes_to_str from .collection import DynamicScopeCollection, ScopeCollection, StaticScopeCollection from .data import ( AuthScopes, @@ -36,6 +35,4 @@ "SearchScopes", "TimersScopes", "TransferScopes", - "scopes_to_str", - "scopes_to_scope_list", ) diff --git a/src/globus_sdk/scopes/_normalize.py b/src/globus_sdk/scopes/_normalize.py deleted file mode 100644 index f907ca3cb..000000000 --- a/src/globus_sdk/scopes/_normalize.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -import typing as t - -from .parser import ScopeParser -from .representation import Scope - - -def scopes_to_scope_list(scopes: str | Scope | t.Iterable[str | Scope]) -> list[Scope]: - """ - Normalize scopes to a list of Scope 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: - - .. code-block:: pycon - - >>> scopes_to_scope_list(Scope("foo")) - [Scope('foo')] - >>> scopes_to_scope_list(Scope("foo"), "bar baz", Scope("qux")) - [Scope('foo'), Scope('bar'), Scope('baz'), Scope('qux')] - """ - scope_list: list[Scope] = [] - for scope in _iter_scope_collection(scopes): - if isinstance(scope, str): - scope_list.extend(ScopeParser.parse(scope)) - else: - scope_list.append(scope) - return scope_list - - -def _iter_scope_collection( - obj: str | Scope | t.Iterable[str | Scope], -) -> t.Iterator[str | Scope]: - """ - 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 string, scope object, or an iterable of scope strings - and scope objects. - Example usage: - - .. code-block:: pycon - - >>> list(_iter_scope_collection("foo")) - ['foo'] - >>> 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]'] - """ - if isinstance(obj, str): - yield from _iter_scope_string(obj) - elif isinstance(obj, Scope): - yield obj - else: - yield from _iter_scope_iterable(obj) - - -def _iter_scope_string(scope_str: str) -> t.Iterator[str]: - if " " not in scope_str: - yield scope_str - elif "[" not in scope_str: - yield from scope_str.split(" ") - else: - for scope_obj in ScopeParser.parse(scope_str): - yield str(scope_obj) - - -def _iter_scope_iterable( - scope_iterable: t.Iterable[str | Scope], -) -> t.Iterator[str | Scope]: - for scope in scope_iterable: - if isinstance(scope, str): - yield from _iter_scope_string(scope) - elif isinstance(scope, Scope): - yield scope - else: - raise TypeError(f"Expected str or Scope in iterable, got {type(scope)}") diff --git a/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py b/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py index ff25aeb73..f40f66e88 100644 --- a/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py +++ b/tests/non-pytest/mypy-ignore-tests/scope_collection_type.py @@ -1,7 +1,7 @@ import typing as t import globus_sdk -from globus_sdk.scopes import Scope, scopes_to_str +from globus_sdk.scopes import Scope, ScopeParser from globus_sdk.services.auth import ( GlobusAuthorizationCodeFlowManager, GlobusNativeAppFlowManager, @@ -24,7 +24,7 @@ # this function should type-check okay def foo(x: str | Scope | t.Iterable[str | Scope]) -> str: - return scopes_to_str(x) + return ScopeParser.serialize(x) foo("somestring") diff --git a/tests/unit/globus_app/test_scope_normalization.py b/tests/unit/globus_app/test_scope_normalization.py new file mode 100644 index 000000000..c4369691a --- /dev/null +++ b/tests/unit/globus_app/test_scope_normalization.py @@ -0,0 +1,59 @@ +import pytest + +import globus_sdk +from globus_sdk.scopes import Scope + + +@pytest.fixture +def user_app(): + client_id = "mock_client_id" + return globus_sdk.UserApp("test-app", client_id=client_id) + + +@pytest.mark.parametrize( + "scope_collection", + ([Scope("scope1")], Scope("scope1"), "scope1", ["scope1"]), +) +def test_iter_scopes_simple(user_app, scope_collection): + actual_list = list(user_app._iter_scopes(scope_collection)) + + assert len(actual_list) == 1 + assert isinstance(actual_list[0], Scope) + assert str(actual_list[0]) == "scope1" + + +@pytest.mark.parametrize( + "scope_collection, expect_str", + ( + (("scope1", "scope2"), "scope1 scope2"), + (("scope1", Scope("scope2")), "scope1 scope2"), + ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), + ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), + ( + (Scope("scope1"), Scope("scope2"), "scope3 scope4"), + "scope1 scope2 scope3 scope4", + ), + ([Scope("scope1"), "scope2", "scope3 scope4"], "scope1 scope2 scope3 scope4"), + ), +) +def test_iter_scopes_handles_mixed_data(user_app, scope_collection, expect_str): + actual_list = list(user_app._iter_scopes(scope_collection)) + + assert all(isinstance(scope, Scope) for scope in actual_list) + assert _as_sorted_string(actual_list) == expect_str + + +def test_iter_scopes_handles_dependent_scopes(user_app): + scope_collection = "scope1 scope2[scope3 scope4]" + actual_list = list(user_app._iter_scopes(scope_collection)) + + actual_sorted_str = _as_sorted_string(actual_list) + # Dependent scope ordering is not guaranteed + assert ( + actual_sorted_str == "scope1 scope2[scope3 scope4]" + or actual_sorted_str == "scope1 scope2[scope4 scope3]" + ) + + +def _as_sorted_string(scope_list) -> str: + return " ".join(sorted(str(scope) for scope in scope_list)) diff --git a/tests/unit/scopes/test_scope_normalization.py b/tests/unit/scopes/test_scope_normalization.py deleted file mode 100644 index 1b12598a1..000000000 --- a/tests/unit/scopes/test_scope_normalization.py +++ /dev/null @@ -1,114 +0,0 @@ -import pytest - -from globus_sdk.scopes import Scope, scopes_to_scope_list, scopes_to_str - - -def test_scopes_to_str_roundtrip_simple_str(): - assert scopes_to_str("scope1") == "scope1" - - -def test_scopes_to_str_stringifies_single_scope(): - assert scopes_to_str(Scope("scope1")) == "scope1" - - -@pytest.mark.parametrize( - "scope_collection", - ( - ("scope1",), - ["scope1"], - {"scope1"}, - (s for s in ["scope1"]), - ), -) -def test_scopes_to_str_roundtrip_simple_str_in_collection(scope_collection): - assert scopes_to_str(scope_collection) == "scope1" - - -@pytest.mark.parametrize( - "scope_collection, expect_str", - ( - (("scope1", Scope("scope2")), "scope1 scope2"), - ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), - ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), - ( - (Scope("scope1"), Scope("scope2"), "scope3 scope4"), - "scope1 scope2 scope3 scope4", - ), - ([Scope("scope1"), "scope2", "scope3 scope4"], "scope1 scope2 scope3 scope4"), - ), -) -def test_scopes_to_str_handles_mixed_data(scope_collection, expect_str): - assert scopes_to_str(scope_collection) == expect_str - - -@pytest.mark.parametrize( - "scope_collection", - ( - ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), - [["bar"]], - ), -) -def test_scopes_to_str_rejects_nested_iterables(scope_collection): - with pytest.raises(TypeError): - scopes_to_str(scope_collection) - - -@pytest.mark.parametrize( - "scope_collection", - ([Scope("scope1")], Scope("scope1"), "scope1"), -) -def test_scopes_to_scope_list_simple(scope_collection): - actual_list = scopes_to_scope_list(scope_collection) - - assert len(actual_list) == 1 - assert isinstance(actual_list[0], Scope) - assert str(actual_list[0]) == "scope1" - - -@pytest.mark.parametrize( - "scope_collection, expect_str", - ( - (("scope1", "scope2"), "scope1 scope2"), - (("scope1", Scope("scope2")), "scope1 scope2"), - ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), - ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), - ( - (Scope("scope1"), Scope("scope2"), "scope3 scope4"), - "scope1 scope2 scope3 scope4", - ), - ([Scope("scope1"), "scope2", "scope3 scope4"], "scope1 scope2 scope3 scope4"), - ), -) -def test_scopes_to_scope_list_handles_mixed_data(scope_collection, expect_str): - actual_list = scopes_to_scope_list(scope_collection) - - assert all(isinstance(scope, Scope) for scope in actual_list) - assert _as_sorted_string(actual_list) == expect_str - - -@pytest.mark.parametrize( - "scope_collection", - ( - ((Scope("scope1"), Scope("scope2")), "scope3 scope4"), - [["bar"]], - ), -) -def test_scopes_to_list_rejects_nested_iterables(scope_collection): - with pytest.raises(TypeError): - scopes_to_scope_list(scope_collection) - - -def test_scopes_to_scope_list_handles_dependent_scopes(): - scope_collection = "scope1 scope2[scope3 scope4]" - actual_list = scopes_to_scope_list(scope_collection) - - actual_sorted_str = _as_sorted_string(actual_list) - # Dependent scope ordering is not guaranteed - assert ( - actual_sorted_str == "scope1 scope2[scope3 scope4]" - or actual_sorted_str == "scope1 scope2[scope4 scope3]" - ) - - -def _as_sorted_string(scope_list) -> str: - return " ".join(sorted(str(scope) for scope in scope_list)) diff --git a/tests/unit/scopes/test_scope_parser.py b/tests/unit/scopes/test_scope_parser.py index b0f7e2b53..bcc5db2c6 100644 --- a/tests/unit/scopes/test_scope_parser.py +++ b/tests/unit/scopes/test_scope_parser.py @@ -200,3 +200,51 @@ def test_scope_init_forbids_special_chars(scope_str): ) def test_scope_parsing_normalizes_optionals(original, reserialized): assert {str(s) for s in ScopeParser.parse(original)} == reserialized + + +@pytest.mark.parametrize( + "scope_str", + ( + "foo", + "*foo", + "foo[bar] baz", + " foo ", + "foo[bar] bar[foo]", + ), +) +def test_serialize_of_scope_string_is_exact(scope_str): + assert ScopeParser.serialize(scope_str) == scope_str + + +def test_serialize_of_scope_object(): + assert ScopeParser.serialize(Scope("scope1")) == "scope1" + + +@pytest.mark.parametrize( + "scope_collection", + ( + ("scope1",), + ["scope1"], + {"scope1"}, + (s for s in ["scope1"]), + ), +) +def test_serialize_of_simple_collection_of_strings(scope_collection): + assert ScopeParser.serialize(scope_collection) == "scope1" + + +@pytest.mark.parametrize( + "scope_collection, expect_str", + ( + (("scope1", Scope("scope2")), "scope1 scope2"), + ((Scope("scope1"), Scope("scope2")), "scope1 scope2"), + ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"), + ( + (Scope("scope1"), Scope("scope2"), "scope3 scope4"), + "scope1 scope2 scope3 scope4", + ), + ([Scope("scope1"), "scope2", "scope3 scope4"], "scope1 scope2 scope3 scope4"), + ), +) +def test_serialize_handles_mixed_data(scope_collection, expect_str): + assert ScopeParser.serialize(scope_collection) == expect_str From 5a6ac1475388ee91d9c7ae7c4a1d43d6ae047fee Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 25 Jul 2025 17:36:22 -0500 Subject: [PATCH 137/176] Make ScopeParser.serialize handle the empty case Rather than a helper specific to Globus Auth components doing this as a post-processing step, handling for `""` as the output is now baked into `ScopeParser.serialize`. By default, it will raise an error if it finds that the serialized data is `""`. For example, `ScopeParser.serialize([])` errors. A new flag, `reject_empty=False`, can be passed to explicitly disable the check, if users have a desire to handle an unknown iterable and check if it's empty *after* serialization. --- src/globus_sdk/scopes/parser.py | 16 +++++++-- src/globus_sdk/services/auth/_common.py | 13 ------- .../auth/client/confidential_client.py | 5 ++- .../auth/flow_managers/authorization_code.py | 5 ++- .../services/auth/flow_managers/native_app.py | 5 ++- .../unit/helpers/test_auth_scope_stringify.py | 34 ------------------- tests/unit/scopes/test_scope_parser.py | 15 ++++++++ 7 files changed, 35 insertions(+), 58 deletions(-) delete mode 100644 tests/unit/helpers/test_auth_scope_stringify.py diff --git a/src/globus_sdk/scopes/parser.py b/src/globus_sdk/scopes/parser.py index c2e7b19d9..c95d5e996 100644 --- a/src/globus_sdk/scopes/parser.py +++ b/src/globus_sdk/scopes/parser.py @@ -2,6 +2,8 @@ import typing as t +from globus_sdk import exc + from ._graph_parser import ScopeGraph from .representation import Scope @@ -76,7 +78,9 @@ def merge_scopes(cls, scopes_a: list[Scope], scopes_b: list[Scope]) -> list[Scop ) @classmethod - def serialize(cls, scopes: str | Scope | t.Iterable[str | Scope]) -> str: + def serialize( + cls, scopes: str | Scope | t.Iterable[str | Scope], *, reject_empty: bool = True + ) -> str: """ Normalize scopes to a space-separated scope string. @@ -85,6 +89,8 @@ def serialize(cls, scopes: str | Scope | t.Iterable[str | Scope]) -> str: :param scopes: A scope string, scope object, or an iterable of scope strings and scope objects. + :param reject_empty: When true (the default), raise an error if the + scopes serialize to the empty string. :returns: A space-separated scope string. Example usage: @@ -99,4 +105,10 @@ def serialize(cls, scopes: str | Scope | t.Iterable[str | Scope]) -> str: scope_iter = (scopes,) else: scope_iter = scopes - return " ".join(str(scope) for scope in scope_iter) + + result = " ".join(str(scope) for scope in scope_iter) + if reject_empty and result == "": + raise exc.GlobusSDKUsageError( + "'scopes' cannot be the empty string or empty collection." + ) + return result diff --git a/src/globus_sdk/services/auth/_common.py b/src/globus_sdk/services/auth/_common.py index aa83419de..c6144ff50 100644 --- a/src/globus_sdk/services/auth/_common.py +++ b/src/globus_sdk/services/auth/_common.py @@ -8,24 +8,11 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from globus_sdk._missing import MISSING, MissingType -from globus_sdk.exc import GlobusSDKUsageError from globus_sdk.response import GlobusHTTPResponse -from globus_sdk.scopes import Scope, ScopeParser log = logging.getLogger(__name__) -def stringify_requested_scopes( - requested_scopes: str | Scope | t.Iterable[str | Scope], -) -> str: - requested_scopes_string: str = ScopeParser.serialize(requested_scopes) - if requested_scopes_string == "": - raise GlobusSDKUsageError( - "requested_scopes cannot be the empty string or empty collection" - ) - return requested_scopes_string - - class _JWKGetCallbackProto(t.Protocol): def __call__( self, diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index decb6a7b5..4783a71c4 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -9,9 +9,8 @@ from globus_sdk._missing import MISSING, MissingType from globus_sdk.authorizers import BasicAuthorizer from globus_sdk.response import GlobusHTTPResponse -from globus_sdk.scopes import Scope +from globus_sdk.scopes import Scope, ScopeParser -from .._common import stringify_requested_scopes from ..flow_managers import GlobusAuthorizationCodeFlowManager from ..response import OAuthClientCredentialsResponse, OAuthDependentTokenResponse from .base_login_client import AuthLoginClient @@ -83,7 +82,7 @@ def oauth2_client_credentials_tokens( >>> transfer_token_info = tokens.by_resource_server["transfer.api.globus.org"] >>> transfer_token = transfer_token_info["access_token"] """ # noqa: E501 - requested_scopes_string = stringify_requested_scopes(requested_scopes) + requested_scopes_string = ScopeParser.serialize(requested_scopes) log.debug( "Fetching token(s) using client credentials, " f"scope={requested_scopes_string}" diff --git a/src/globus_sdk/services/auth/flow_managers/authorization_code.py b/src/globus_sdk/services/auth/flow_managers/authorization_code.py index 9e2a62c1e..3c09e636f 100644 --- a/src/globus_sdk/services/auth/flow_managers/authorization_code.py +++ b/src/globus_sdk/services/auth/flow_managers/authorization_code.py @@ -6,9 +6,8 @@ from globus_sdk._internal.utils import slash_join from globus_sdk._missing import filter_missing -from globus_sdk.scopes import Scope +from globus_sdk.scopes import Scope, ScopeParser -from .._common import stringify_requested_scopes from ..response import OAuthAuthorizationCodeResponse from .base import GlobusOAuthFlowManager @@ -56,7 +55,7 @@ def __init__( ) -> None: # convert a scope object or iterable to string immediately on load # and default to the default requested scopes - self.requested_scopes: str = stringify_requested_scopes(requested_scopes) + self.requested_scopes: str = ScopeParser.serialize(requested_scopes) # store the remaining parameters directly, with no transformation self.client_id = auth_client.client_id diff --git a/src/globus_sdk/services/auth/flow_managers/native_app.py b/src/globus_sdk/services/auth/flow_managers/native_app.py index b65e68f1d..39e09d6ef 100644 --- a/src/globus_sdk/services/auth/flow_managers/native_app.py +++ b/src/globus_sdk/services/auth/flow_managers/native_app.py @@ -11,9 +11,8 @@ from globus_sdk._internal.utils import slash_join from globus_sdk._missing import MISSING, MissingType, filter_missing from globus_sdk.exc import GlobusSDKUsageError -from globus_sdk.scopes import Scope +from globus_sdk.scopes import Scope, ScopeParser -from .._common import stringify_requested_scopes from ..response import OAuthAuthorizationCodeResponse from .base import GlobusOAuthFlowManager @@ -125,7 +124,7 @@ def __init__( ) # convert scopes iterable to string immediately on load - self.requested_scopes = stringify_requested_scopes(requested_scopes) + self.requested_scopes = ScopeParser.serialize(requested_scopes) # default to `/v2/web/auth-code` on whatever environment we're looking # at -- most typically it will be `https://auth.globus.org/` diff --git a/tests/unit/helpers/test_auth_scope_stringify.py b/tests/unit/helpers/test_auth_scope_stringify.py deleted file mode 100644 index d95622cb4..000000000 --- a/tests/unit/helpers/test_auth_scope_stringify.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from globus_sdk import GlobusSDKUsageError -from globus_sdk.scopes import Scope -from globus_sdk.services.auth._common import stringify_requested_scopes - - -def test_scope_stringify_roundtrips_string(): - assert stringify_requested_scopes("foo") == "foo" - - -def test_scope_stringify_matches_str_of_scope_object(): - foo_scope = Scope("foo") - # these asserts are nearly equivalent, but not quite the same - # Scope.__str__ could -- at least, in theory -- change in the future - assert stringify_requested_scopes(foo_scope) == str(foo_scope) - assert stringify_requested_scopes(foo_scope) == "foo" - - -def test_scope_stringify_rejects_empty_string(): - with pytest.raises( - GlobusSDKUsageError, - match="requested_scopes cannot be the empty string or empty collection", - ): - stringify_requested_scopes("") - - -@pytest.mark.parametrize("collection_obj", ([], set(), ())) -def test_scope_stringify_rejects_empty_collection(collection_obj): - with pytest.raises( - GlobusSDKUsageError, - match="requested_scopes cannot be the empty string or empty collection", - ): - stringify_requested_scopes(collection_obj) diff --git a/tests/unit/scopes/test_scope_parser.py b/tests/unit/scopes/test_scope_parser.py index bcc5db2c6..0284685c1 100644 --- a/tests/unit/scopes/test_scope_parser.py +++ b/tests/unit/scopes/test_scope_parser.py @@ -2,6 +2,7 @@ import pytest +from globus_sdk import exc from globus_sdk.scopes import Scope, ScopeCycleError, ScopeParseError, ScopeParser @@ -248,3 +249,17 @@ def test_serialize_of_simple_collection_of_strings(scope_collection): ) def test_serialize_handles_mixed_data(scope_collection, expect_str): assert ScopeParser.serialize(scope_collection) == expect_str + + +@pytest.mark.parametrize("input_obj", ("", [], set(), ())) +def test_serialize_rejects_empty_by_default(input_obj): + with pytest.raises( + exc.GlobusSDKUsageError, + match="'scopes' cannot be the empty string or empty collection", + ): + ScopeParser.serialize(input_obj) + + +@pytest.mark.parametrize("input_obj", ("", [], set(), ())) +def test_serialize_allows_empty_string_with_flag(input_obj): + assert ScopeParser.serialize(input_obj, reject_empty=False) == "" From 6b710e7050632fc88448f5d8ec23189d0c8e374b Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 25 Jul 2025 17:40:29 -0500 Subject: [PATCH 138/176] Add a changelog for scope normalization updates --- ...25_172504_sirosen_cleanup_scope_normalization.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst diff --git a/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst b/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst new file mode 100644 index 000000000..b61e39621 --- /dev/null +++ b/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst @@ -0,0 +1,12 @@ +Breaking Changes +---------------- + +- Interfaces for normalizing scope data have changed. (:pr:`NUMBER`) + + - The ``scopes_to_str`` function has been replaced with + ``ScopeParser.serialize``. + + - ``ScopeParser.serialize`` will raise an error if the serialized data is + empty. A flag, ``reject_empty=False``, can be passed to disable this check. + + - The ``scopes_to_scope_list`` function has been removed. From 98711f3db050eda7d7367f546680930dbee4b103 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 17 Jul 2025 15:52:13 -0500 Subject: [PATCH 139/176] Make legacy token storage less accessible 1. Rename the `v1` subpackage to `legacy` 2. Update the top-level `globus_sdk.token_storage` interface to only list "v2" (i.e. non-legacy) interfaces A new section is added to the upgrading guide on the changes in names (not previously covered). --- ...17_154854_sirosen_bury_v1_tokenstorage.rst | 8 ++++ .../token_caching/storage_adapters.rst | 15 ++----- docs/examples/group_listing.rst | 6 +-- docs/upgrading.rst | 39 +++++++++++++++++++ src/globus_sdk/token_storage/__init__.py | 17 +------- .../token_storage/{v1 => legacy}/__init__.py | 0 .../token_storage/{v1 => legacy}/base.py | 0 .../{v1 => legacy}/file_adapters.py | 0 .../{v1 => legacy}/memory_adapter.py | 0 .../{v1 => legacy}/sqlite_adapter.py | 0 .../tokenstorage/v1/test_simplejson_file.py | 2 +- .../functional/tokenstorage/v1/test_sqlite.py | 2 +- .../tokenstorage/v2/test_json_tokenstorage.py | 3 +- .../v2/test_sqlite_tokenstorage.py | 3 +- .../tokenstorage/v1/test_memory_adapter.py | 2 +- .../v1/test_simplejson_adapter.py | 2 +- .../tokenstorage/v1/test_sqlite_adapter.py | 2 +- 17 files changed, 65 insertions(+), 36 deletions(-) create mode 100644 changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst rename src/globus_sdk/token_storage/{v1 => legacy}/__init__.py (100%) rename src/globus_sdk/token_storage/{v1 => legacy}/base.py (100%) rename src/globus_sdk/token_storage/{v1 => legacy}/file_adapters.py (100%) rename src/globus_sdk/token_storage/{v1 => legacy}/memory_adapter.py (100%) rename src/globus_sdk/token_storage/{v1 => legacy}/sqlite_adapter.py (100%) diff --git a/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst b/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst new file mode 100644 index 000000000..78ab5490c --- /dev/null +++ b/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst @@ -0,0 +1,8 @@ +Changed +------- + +- The legacy token storage adapters are now only available from the + ``globus_sdk.token_storage.legacy`` subpackage. + + Users are encouraged to migrate to the newer tooling available directly from + ``globus_sdk.token_storage``. (:pr:`NUMBER`) diff --git a/docs/authorization/token_caching/storage_adapters.rst b/docs/authorization/token_caching/storage_adapters.rst index 03ccf244e..a7969250a 100644 --- a/docs/authorization/token_caching/storage_adapters.rst +++ b/docs/authorization/token_caching/storage_adapters.rst @@ -15,7 +15,7 @@ received from authentication and token refreshes. Usage ----- -StorageAdapter is available under the name ``globus_sdk.token_storage``. +StorageAdapter is available under the name ``globus_sdk.token_storage.legacy``. Storage adapters are the main objects of this subpackage. Primarily, usage should revolve around creating a storage adapter, potentially loading data from @@ -27,7 +27,7 @@ For example: import os import globus_sdk - from globus_sdk.token_storage import SimpleJSONFileAdapter + from globus_sdk.token_storage.legacy import SimpleJSONFileAdapter my_file_adapter = SimpleJSONFileAdapter(os.path.expanduser("~/mytokens.json")) @@ -76,19 +76,12 @@ For example: tc = globus_sdk.TransferClient(authorizer=authorizer) -Complete Example Usage -~~~~~~~~~~~~~~~~~~~~~~ - -The :ref:`Group Listing With Token Storage Script ` -provides a complete and runnable example which leverages ``token_storage``. - - Adapter Types ------------- -.. module:: globus_sdk.token_storage +.. module:: globus_sdk.token_storage.legacy -``globus_sdk.token_storage`` provides base classes for building your own storage +``globus_sdk.token_storage.legacy`` provides base classes for building your own storage adapters, and several complete adapters. The :class:`SimpleJSONFileAdapter` is good for the "simplest possible" diff --git a/docs/examples/group_listing.rst b/docs/examples/group_listing.rst index e218b6286..0e9ca4724 100644 --- a/docs/examples/group_listing.rst +++ b/docs/examples/group_listing.rst @@ -62,8 +62,8 @@ For simplicity, the script will prompt for login on each use. Group Listing With Token Storage -------------------------------- -``globus_sdk.token_storage`` provides tools for managing refresh tokens. The -following example script shows how you might use this to provide a complete +``globus_sdk.token_storage.legacy`` provides tools for managing refresh tokens. +The following example script shows how you might use this to provide a complete script which lists the current user's groups using refresh tokens. @@ -72,7 +72,7 @@ script which lists the current user's groups using refresh tokens. import os from globus_sdk import GroupsClient, NativeAppAuthClient, RefreshTokenAuthorizer - from globus_sdk.token_storage import SimpleJSONFileAdapter + from globus_sdk.token_storage.legacy import SimpleJSONFileAdapter CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" AUTH_CLIENT = NativeAppAuthClient(CLIENT_ID) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index f8fbd41c3..d9da53640 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -262,6 +262,45 @@ In version 4, this has been removed, but the collection types provide scopes for the Globus Transfer service via ``list(TransferClient.scopes)`` or similar usage. +Token Storage Subpackage Renamed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The subpackage providing token storage components has been renamed and slightly +restructured. + +The package name is changed from +``globus_sdk.tokenstorage`` to ``globus_sdk.token_storage``. + +Furthermore, the legacy :ref:`storage adapters ` are now only +available from ``globus_sdk.token_storage.legacy``. + +Therefore, usages of the modern :ref:`token storage interface ` +should update like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.tokenstorage import JSONTokenStorage + + # globus-sdk v4 + from globus_sdk.token_storage import JSONTokenStorage + +For legacy adapter usage, update like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.tokenstorage import SimpleJSONFileAdapter + + # globus-sdk v4 + from globus_sdk.token_storage.legacy import SimpleJSONFileAdapter + +.. note:: + + The ``legacy`` interface is soft-deprecated. + In version 4.0.0 it will not emit deprecation warnings. + Future SDK versions will eventually deprecate and remove these interfaces. + Deprecated Timers Aliases Removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/globus_sdk/token_storage/__init__.py b/src/globus_sdk/token_storage/__init__.py index 2f6a81e07..c3e4feb5e 100644 --- a/src/globus_sdk/token_storage/__init__.py +++ b/src/globus_sdk/token_storage/__init__.py @@ -1,10 +1,3 @@ -from .v1 import ( - FileAdapter, - MemoryAdapter, - SimpleJSONFileAdapter, - SQLiteAdapter, - StorageAdapter, -) from .v2 import ( FileTokenStorage, HasRefreshTokensValidator, @@ -23,20 +16,14 @@ ) __all__ = ( - # [v1] "StorageAdapter" Constructs - "StorageAdapter", - "FileAdapter", - "SimpleJSONFileAdapter", - "SQLiteAdapter", - "MemoryAdapter", - # [v2] "TokenStorage" Constructs + # "TokenStorage" Constructs "TokenStorage", "TokenStorageData", "FileTokenStorage", "JSONTokenStorage", "SQLiteTokenStorage", "MemoryTokenStorage", - # [v2] "ValidatingTokenStorage" Constructs + # "ValidatingTokenStorage" Constructs "ValidatingTokenStorage", "TokenValidationContext", "TokenDataValidator", diff --git a/src/globus_sdk/token_storage/v1/__init__.py b/src/globus_sdk/token_storage/legacy/__init__.py similarity index 100% rename from src/globus_sdk/token_storage/v1/__init__.py rename to src/globus_sdk/token_storage/legacy/__init__.py diff --git a/src/globus_sdk/token_storage/v1/base.py b/src/globus_sdk/token_storage/legacy/base.py similarity index 100% rename from src/globus_sdk/token_storage/v1/base.py rename to src/globus_sdk/token_storage/legacy/base.py diff --git a/src/globus_sdk/token_storage/v1/file_adapters.py b/src/globus_sdk/token_storage/legacy/file_adapters.py similarity index 100% rename from src/globus_sdk/token_storage/v1/file_adapters.py rename to src/globus_sdk/token_storage/legacy/file_adapters.py diff --git a/src/globus_sdk/token_storage/v1/memory_adapter.py b/src/globus_sdk/token_storage/legacy/memory_adapter.py similarity index 100% rename from src/globus_sdk/token_storage/v1/memory_adapter.py rename to src/globus_sdk/token_storage/legacy/memory_adapter.py diff --git a/src/globus_sdk/token_storage/v1/sqlite_adapter.py b/src/globus_sdk/token_storage/legacy/sqlite_adapter.py similarity index 100% rename from src/globus_sdk/token_storage/v1/sqlite_adapter.py rename to src/globus_sdk/token_storage/legacy/sqlite_adapter.py diff --git a/tests/functional/tokenstorage/v1/test_simplejson_file.py b/tests/functional/tokenstorage/v1/test_simplejson_file.py index 371784f11..b2eb8730c 100644 --- a/tests/functional/tokenstorage/v1/test_simplejson_file.py +++ b/tests/functional/tokenstorage/v1/test_simplejson_file.py @@ -4,7 +4,7 @@ import pytest from globus_sdk import __version__ -from globus_sdk.token_storage import SimpleJSONFileAdapter +from globus_sdk.token_storage.legacy import SimpleJSONFileAdapter IS_WINDOWS = os.name == "nt" diff --git a/tests/functional/tokenstorage/v1/test_sqlite.py b/tests/functional/tokenstorage/v1/test_sqlite.py index 95361b5cf..ebcaca67e 100644 --- a/tests/functional/tokenstorage/v1/test_sqlite.py +++ b/tests/functional/tokenstorage/v1/test_sqlite.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk.token_storage import SQLiteAdapter +from globus_sdk.token_storage.legacy import SQLiteAdapter @pytest.fixture diff --git a/tests/functional/tokenstorage/v2/test_json_tokenstorage.py b/tests/functional/tokenstorage/v2/test_json_tokenstorage.py index b383cf862..dc78b4f6b 100644 --- a/tests/functional/tokenstorage/v2/test_json_tokenstorage.py +++ b/tests/functional/tokenstorage/v2/test_json_tokenstorage.py @@ -4,7 +4,8 @@ import pytest from globus_sdk import __version__ -from globus_sdk.token_storage import JSONTokenStorage, SimpleJSONFileAdapter +from globus_sdk.token_storage import JSONTokenStorage +from globus_sdk.token_storage.legacy import SimpleJSONFileAdapter IS_WINDOWS = os.name == "nt" diff --git a/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py b/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py index 552f3a2d1..21c505efb 100644 --- a/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py +++ b/tests/functional/tokenstorage/v2/test_sqlite_tokenstorage.py @@ -1,7 +1,8 @@ import pytest from globus_sdk import exc -from globus_sdk.token_storage import SQLiteAdapter, SQLiteTokenStorage +from globus_sdk.token_storage import SQLiteTokenStorage +from globus_sdk.token_storage.legacy import SQLiteAdapter @pytest.fixture diff --git a/tests/unit/tokenstorage/v1/test_memory_adapter.py b/tests/unit/tokenstorage/v1/test_memory_adapter.py index 9402eaaac..9a8bd0390 100644 --- a/tests/unit/tokenstorage/v1/test_memory_adapter.py +++ b/tests/unit/tokenstorage/v1/test_memory_adapter.py @@ -1,7 +1,7 @@ import time from unittest import mock -from globus_sdk.token_storage import MemoryAdapter +from globus_sdk.token_storage.legacy import MemoryAdapter def test_memory_adapter_store_overwrites_only_new_data(): diff --git a/tests/unit/tokenstorage/v1/test_simplejson_adapter.py b/tests/unit/tokenstorage/v1/test_simplejson_adapter.py index 88d35d28c..fd09b238a 100644 --- a/tests/unit/tokenstorage/v1/test_simplejson_adapter.py +++ b/tests/unit/tokenstorage/v1/test_simplejson_adapter.py @@ -3,7 +3,7 @@ import pytest from globus_sdk import __version__ as sdkversion -from globus_sdk.token_storage import SimpleJSONFileAdapter +from globus_sdk.token_storage.legacy import SimpleJSONFileAdapter def test_simplejson_reading_bad_data(tmp_path): diff --git a/tests/unit/tokenstorage/v1/test_sqlite_adapter.py b/tests/unit/tokenstorage/v1/test_sqlite_adapter.py index d7740af05..be0cb5cb7 100644 --- a/tests/unit/tokenstorage/v1/test_sqlite_adapter.py +++ b/tests/unit/tokenstorage/v1/test_sqlite_adapter.py @@ -1,6 +1,6 @@ import pytest -from globus_sdk.token_storage import SQLiteAdapter +from globus_sdk.token_storage.legacy import SQLiteAdapter def test_sqlite_reading_bad_config(): From 8732e91d42bda3823d0b393d4f8b22324c68ae0d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 28 Jul 2025 12:54:29 -0500 Subject: [PATCH 140/176] Move 'v2' token_storage up a level All of the 'v2' entities move up one nesting level, now that the 'v1' items are renamed as 'legacy'. --- .../globus_app/authorizer_factory.py | 2 +- src/globus_sdk/globus_app/config.py | 2 +- src/globus_sdk/token_storage/__init__.py | 16 ++++----- src/globus_sdk/token_storage/{v2 => }/base.py | 0 src/globus_sdk/token_storage/{v2 => }/json.py | 0 .../token_storage/{v2 => }/memory.py | 0 .../token_storage/{v2 => }/sqlite.py | 0 .../token_storage/{v2 => }/token_data.py | 0 src/globus_sdk/token_storage/v2/__init__.py | 33 ------------------- .../validating_token_storage/__init__.py | 0 .../validating_token_storage/context.py | 0 .../validating_token_storage/errors.py | 0 .../validating_token_storage/storage.py | 0 .../validating_token_storage/validators.py | 0 .../globus_app/test_authorizer_factory.py | 2 +- .../tokenstorage/v2/test_token_storage.py | 2 +- .../v2/test_validating_token_storage.py | 2 +- 17 files changed, 12 insertions(+), 47 deletions(-) rename src/globus_sdk/token_storage/{v2 => }/base.py (100%) rename src/globus_sdk/token_storage/{v2 => }/json.py (100%) rename src/globus_sdk/token_storage/{v2 => }/memory.py (100%) rename src/globus_sdk/token_storage/{v2 => }/sqlite.py (100%) rename src/globus_sdk/token_storage/{v2 => }/token_data.py (100%) delete mode 100644 src/globus_sdk/token_storage/v2/__init__.py rename src/globus_sdk/token_storage/{v2 => }/validating_token_storage/__init__.py (100%) rename src/globus_sdk/token_storage/{v2 => }/validating_token_storage/context.py (100%) rename src/globus_sdk/token_storage/{v2 => }/validating_token_storage/errors.py (100%) rename src/globus_sdk/token_storage/{v2 => }/validating_token_storage/storage.py (100%) rename src/globus_sdk/token_storage/{v2 => }/validating_token_storage/validators.py (100%) diff --git a/src/globus_sdk/globus_app/authorizer_factory.py b/src/globus_sdk/globus_app/authorizer_factory.py index 349505777..5ed78d4ad 100644 --- a/src/globus_sdk/globus_app/authorizer_factory.py +++ b/src/globus_sdk/globus_app/authorizer_factory.py @@ -13,7 +13,7 @@ ) from globus_sdk.services.auth import OAuthTokenResponse from globus_sdk.token_storage import ValidatingTokenStorage -from globus_sdk.token_storage.v2.validating_token_storage import MissingTokenError +from globus_sdk.token_storage.validating_token_storage import MissingTokenError GA = t.TypeVar("GA", bound=GlobusAuthorizer) diff --git a/src/globus_sdk/globus_app/config.py b/src/globus_sdk/globus_app/config.py index 22f01f3db..8c713ee1b 100644 --- a/src/globus_sdk/globus_app/config.py +++ b/src/globus_sdk/globus_app/config.py @@ -17,7 +17,7 @@ TokenStorage, TokenValidationError, ) -from globus_sdk.token_storage.v2.validating_token_storage import IdentityMismatchError +from globus_sdk.token_storage.validating_token_storage import IdentityMismatchError from .protocols import ( IDTokenDecoderProvider, diff --git a/src/globus_sdk/token_storage/__init__.py b/src/globus_sdk/token_storage/__init__.py index c3e4feb5e..707f0798c 100644 --- a/src/globus_sdk/token_storage/__init__.py +++ b/src/globus_sdk/token_storage/__init__.py @@ -1,14 +1,13 @@ -from .v2 import ( - FileTokenStorage, +from .base import FileTokenStorage, TokenStorage +from .json import JSONTokenStorage +from .memory import MemoryTokenStorage +from .sqlite import SQLiteTokenStorage +from .token_data import TokenStorageData +from .validating_token_storage import ( HasRefreshTokensValidator, - JSONTokenStorage, - MemoryTokenStorage, NotExpiredValidator, ScopeRequirementsValidator, - SQLiteTokenStorage, TokenDataValidator, - TokenStorage, - TokenStorageData, TokenValidationContext, TokenValidationError, UnchangingIdentityIDValidator, @@ -16,14 +15,13 @@ ) __all__ = ( - # "TokenStorage" Constructs "TokenStorage", "TokenStorageData", "FileTokenStorage", "JSONTokenStorage", "SQLiteTokenStorage", "MemoryTokenStorage", - # "ValidatingTokenStorage" Constructs + # TokenValidationStorage constructs "ValidatingTokenStorage", "TokenValidationContext", "TokenDataValidator", diff --git a/src/globus_sdk/token_storage/v2/base.py b/src/globus_sdk/token_storage/base.py similarity index 100% rename from src/globus_sdk/token_storage/v2/base.py rename to src/globus_sdk/token_storage/base.py diff --git a/src/globus_sdk/token_storage/v2/json.py b/src/globus_sdk/token_storage/json.py similarity index 100% rename from src/globus_sdk/token_storage/v2/json.py rename to src/globus_sdk/token_storage/json.py diff --git a/src/globus_sdk/token_storage/v2/memory.py b/src/globus_sdk/token_storage/memory.py similarity index 100% rename from src/globus_sdk/token_storage/v2/memory.py rename to src/globus_sdk/token_storage/memory.py diff --git a/src/globus_sdk/token_storage/v2/sqlite.py b/src/globus_sdk/token_storage/sqlite.py similarity index 100% rename from src/globus_sdk/token_storage/v2/sqlite.py rename to src/globus_sdk/token_storage/sqlite.py diff --git a/src/globus_sdk/token_storage/v2/token_data.py b/src/globus_sdk/token_storage/token_data.py similarity index 100% rename from src/globus_sdk/token_storage/v2/token_data.py rename to src/globus_sdk/token_storage/token_data.py diff --git a/src/globus_sdk/token_storage/v2/__init__.py b/src/globus_sdk/token_storage/v2/__init__.py deleted file mode 100644 index 707f0798c..000000000 --- a/src/globus_sdk/token_storage/v2/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -from .base import FileTokenStorage, TokenStorage -from .json import JSONTokenStorage -from .memory import MemoryTokenStorage -from .sqlite import SQLiteTokenStorage -from .token_data import TokenStorageData -from .validating_token_storage import ( - HasRefreshTokensValidator, - NotExpiredValidator, - ScopeRequirementsValidator, - TokenDataValidator, - TokenValidationContext, - TokenValidationError, - UnchangingIdentityIDValidator, - ValidatingTokenStorage, -) - -__all__ = ( - "TokenStorage", - "TokenStorageData", - "FileTokenStorage", - "JSONTokenStorage", - "SQLiteTokenStorage", - "MemoryTokenStorage", - # TokenValidationStorage constructs - "ValidatingTokenStorage", - "TokenValidationContext", - "TokenDataValidator", - "TokenValidationError", - "HasRefreshTokensValidator", - "NotExpiredValidator", - "ScopeRequirementsValidator", - "UnchangingIdentityIDValidator", -) diff --git a/src/globus_sdk/token_storage/v2/validating_token_storage/__init__.py b/src/globus_sdk/token_storage/validating_token_storage/__init__.py similarity index 100% rename from src/globus_sdk/token_storage/v2/validating_token_storage/__init__.py rename to src/globus_sdk/token_storage/validating_token_storage/__init__.py diff --git a/src/globus_sdk/token_storage/v2/validating_token_storage/context.py b/src/globus_sdk/token_storage/validating_token_storage/context.py similarity index 100% rename from src/globus_sdk/token_storage/v2/validating_token_storage/context.py rename to src/globus_sdk/token_storage/validating_token_storage/context.py diff --git a/src/globus_sdk/token_storage/v2/validating_token_storage/errors.py b/src/globus_sdk/token_storage/validating_token_storage/errors.py similarity index 100% rename from src/globus_sdk/token_storage/v2/validating_token_storage/errors.py rename to src/globus_sdk/token_storage/validating_token_storage/errors.py diff --git a/src/globus_sdk/token_storage/v2/validating_token_storage/storage.py b/src/globus_sdk/token_storage/validating_token_storage/storage.py similarity index 100% rename from src/globus_sdk/token_storage/v2/validating_token_storage/storage.py rename to src/globus_sdk/token_storage/validating_token_storage/storage.py diff --git a/src/globus_sdk/token_storage/v2/validating_token_storage/validators.py b/src/globus_sdk/token_storage/validating_token_storage/validators.py similarity index 100% rename from src/globus_sdk/token_storage/v2/validating_token_storage/validators.py rename to src/globus_sdk/token_storage/validating_token_storage/validators.py diff --git a/tests/unit/globus_app/test_authorizer_factory.py b/tests/unit/globus_app/test_authorizer_factory.py index cb38521d9..3982d0f51 100644 --- a/tests/unit/globus_app/test_authorizer_factory.py +++ b/tests/unit/globus_app/test_authorizer_factory.py @@ -13,7 +13,7 @@ MemoryTokenStorage, NotExpiredValidator, ) -from globus_sdk.token_storage.v2.validating_token_storage import ( +from globus_sdk.token_storage.validating_token_storage import ( ExpiredTokenError, MissingTokenError, ValidatingTokenStorage, diff --git a/tests/unit/tokenstorage/v2/test_token_storage.py b/tests/unit/tokenstorage/v2/test_token_storage.py index 8b25e5548..66f8ab3d3 100644 --- a/tests/unit/tokenstorage/v2/test_token_storage.py +++ b/tests/unit/tokenstorage/v2/test_token_storage.py @@ -1,7 +1,7 @@ import pytest from globus_sdk import GlobusSDKUsageError -from globus_sdk.token_storage.v2.base import _slugify_app_name +from globus_sdk.token_storage.base import _slugify_app_name @pytest.mark.parametrize( diff --git a/tests/unit/tokenstorage/v2/test_validating_token_storage.py b/tests/unit/tokenstorage/v2/test_validating_token_storage.py index f7ab0046a..d1203e960 100644 --- a/tests/unit/tokenstorage/v2/test_validating_token_storage.py +++ b/tests/unit/tokenstorage/v2/test_validating_token_storage.py @@ -22,7 +22,7 @@ UnchangingIdentityIDValidator, ValidatingTokenStorage, ) -from globus_sdk.token_storage.v2.validating_token_storage import ( +from globus_sdk.token_storage.validating_token_storage import ( IdentityMismatchError, MissingIdentityError, MissingTokenError, From 743425ebed1a729dbfe11f861e4fd739620d9a63 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 29 Jul 2025 10:34:20 -0500 Subject: [PATCH 141/176] Remove the 'filter_role' param for list_flows (#1291) --- ...0729_094933_sirosen_remove_filter_role.rst | 5 ++ .../create_and_run_flow/manage_flow.py | 2 +- .../manage_flow_minimal.py | 2 +- src/globus_sdk/services/flows/client.py | 34 ++----------- .../services/flows/test_list_flows.py | 50 +++---------------- 5 files changed, 17 insertions(+), 76 deletions(-) create mode 100644 changelog.d/20250729_094933_sirosen_remove_filter_role.rst diff --git a/changelog.d/20250729_094933_sirosen_remove_filter_role.rst b/changelog.d/20250729_094933_sirosen_remove_filter_role.rst new file mode 100644 index 000000000..5f016c32d --- /dev/null +++ b/changelog.d/20250729_094933_sirosen_remove_filter_role.rst @@ -0,0 +1,5 @@ +Removed +------- + +- Removed the ``filter_role`` parameter to ``FlowsClient.list_flows``. + This parameter was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) diff --git a/docs/examples/create_and_run_flow/manage_flow.py b/docs/examples/create_and_run_flow/manage_flow.py index 1275757f1..8f070c888 100644 --- a/docs/examples/create_and_run_flow/manage_flow.py +++ b/docs/examples/create_and_run_flow/manage_flow.py @@ -94,7 +94,7 @@ def delete_flow(args): def list_flows(): flows_client = get_flows_client() - for flow in flows_client.list_flows(filter_role="flow_owner"): + for flow in flows_client.list_flows(filter_roles="flow_owner"): print(f"title: {flow['title']}") print(f"id: {flow['id']}") print() diff --git a/docs/examples/create_and_run_flow/manage_flow_minimal.py b/docs/examples/create_and_run_flow/manage_flow_minimal.py index e574a50bc..18881ccca 100644 --- a/docs/examples/create_and_run_flow/manage_flow_minimal.py +++ b/docs/examples/create_and_run_flow/manage_flow_minimal.py @@ -86,7 +86,7 @@ def delete_flow(args): def list_flows(): flows_client = get_flows_client() - for flow in flows_client.list_flows(filter_role="flow_owner"): + for flow in flows_client.list_flows(filter_roles="flow_owner"): print(f"title: {flow['title']}") print(f"id: {flow['id']}") print() diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 7cd746b72..8943de904 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -5,13 +5,7 @@ import typing as t import uuid -from globus_sdk import ( - GlobusHTTPResponse, - GlobusSDKUsageError, - client, - exc, - paging, -) +from globus_sdk import GlobusHTTPResponse, client, paging from globus_sdk._internal import guards from globus_sdk._internal.remarshal import commajoin from globus_sdk._missing import MISSING, MissingType @@ -241,7 +235,6 @@ def get_flow( def list_flows( self, *, - filter_role: str | MissingType = MISSING, filter_roles: str | t.Iterable[str] | MissingType = MISSING, filter_fulltext: str | MissingType = MISSING, orderby: str | t.Iterable[str] | MissingType = MISSING, @@ -251,12 +244,8 @@ def list_flows( """ List deployed flows - :param filter_role: (deprecated) A role name specifying the minimum permissions - required for a flow to be included in the response. Mutually exclusive with - **filter_roles**. :param filter_roles: A list of role names specifying the roles the user must - have for a flow to be included in the response. Mutually exclusive with - **filter_role**. + have for a flow to be included in the response. :param filter_fulltext: A string to use in a full-text search to filter results :param orderby: A criterion for ordering flows in the listing :param marker: A marker for pagination @@ -277,15 +266,6 @@ def list_flows( - ``run_monitor`` - ``run_manager`` - .. note:: - - The deprecated ``filter_role`` parameter has similar behavior. - - ``filter_role`` accepts exactly one role name, and filters to flows - where the caller has the specified role or a strictly weaker role. - For example, ``filter_role="flow_administrator"`` will include flows - where the caller has the ``flow_starter`` role. - **OrderBy Values** Values for ``orderby`` consist of a field name, a space, and an @@ -318,7 +298,7 @@ def list_flows( flows = FlowsClient(...) my_frobulate_flows = flows.list_flows( - filter_role="flow_owner", + filter_roles="flow_owner", filter_fulltext="frobulate", orderby=("title ASC", "updated_at DESC"), ) @@ -348,15 +328,7 @@ def list_flows( :service: flows :ref: Flows/paths/~1flows/get """ - if filter_role is not MISSING: - exc.warn_deprecated( - "The `filter_role` parameter is deprecated. Use `filter_roles` instead." - ) - if filter_role is not MISSING and filter_roles is not MISSING: - msg = "Mutually exclusive parameters: filter_role and filter_roles." - raise GlobusSDKUsageError(msg) query_params = { - "filter_role": filter_role, "filter_roles": commajoin(filter_roles), "filter_fulltext": filter_fulltext, # if `orderby` is an iterable (e.g., generator expression), it gets diff --git a/tests/functional/services/flows/test_list_flows.py b/tests/functional/services/flows/test_list_flows.py index 4d7239b81..9632c52e7 100644 --- a/tests/functional/services/flows/test_list_flows.py +++ b/tests/functional/services/flows/test_list_flows.py @@ -2,32 +2,25 @@ import pytest -from globus_sdk import MISSING, GlobusSDKUsageError, RemovedInV4Warning +from globus_sdk import MISSING from globus_sdk.testing import get_last_request, load_response @pytest.mark.parametrize("filter_fulltext", [MISSING, "foo"]) -@pytest.mark.parametrize("filter_role", [MISSING, "bar"]) +@pytest.mark.parametrize("filter_roles", [MISSING, "bar"]) @pytest.mark.parametrize("orderby", [MISSING, "created_at ASC"]) -def test_list_flows_simple(flows_client, filter_fulltext, filter_role, orderby): +def test_list_flows_simple(flows_client, filter_fulltext, filter_roles, orderby): meta = load_response(flows_client.list_flows).metadata add_kwargs = {} if filter_fulltext: add_kwargs["filter_fulltext"] = filter_fulltext - if filter_role: - add_kwargs["filter_role"] = filter_role + if filter_roles: + add_kwargs["filter_roles"] = filter_roles if orderby: add_kwargs["orderby"] = orderby - if filter_role: - with pytest.warns( - RemovedInV4Warning, - match=r"The `filter_role` parameter is deprecated.*", - ): - res = flows_client.list_flows(**add_kwargs) - else: - res = flows_client.list_flows(**add_kwargs) + res = flows_client.list_flows(**add_kwargs) assert res.http_status == 200 # dict-like indexing @@ -42,7 +35,7 @@ def test_list_flows_simple(flows_client, filter_fulltext, filter_role, orderby): k: [v] for k, v in ( ("filter_fulltext", filter_fulltext), - ("filter_role", filter_role), + ("filter_roles", filter_roles), ("orderby", orderby), ) if v is not MISSING @@ -120,35 +113,6 @@ def test_list_flows_orderby_multi(flows_client, orderby_style, orderby_value): assert parsed_qs["orderby"] == expected_orderby_value -@pytest.mark.parametrize( - "filter_role, filter_roles", - [ - # empty string values - ("", ""), - ("", []), - ("", [""]), - # single string values - ("bar", "baz"), - # list values - ("bar", ["baz"]), - ("bar", ["baz", "qux"]), - # comma-separated string - ("bar", "baz,qux"), - # empty list - ("bar", []), - # list containing empty string - ("bar", [""]), - # list containing multiple empty strings - ("bar", ["", ""]), - # list containing mixed values - ("bar", ["baz", "", "qux"]), - ], -) -def test_list_flows_mutually_exclusive_roles(flows_client, filter_role, filter_roles): - with pytest.raises(GlobusSDKUsageError), pytest.warns(RemovedInV4Warning): - flows_client.list_flows(filter_role=filter_role, filter_roles=filter_roles) - - @pytest.mark.parametrize( "filter_roles, expected_filter_roles", [ From 16202e2e3d3a5c8f3787fd6bdb9f2b00abddea1e Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Jul 2025 15:34:32 +0000 Subject: [PATCH 142/176] (actions) update PR references --- changelog.d/20250729_094933_sirosen_remove_filter_role.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250729_094933_sirosen_remove_filter_role.rst b/changelog.d/20250729_094933_sirosen_remove_filter_role.rst index 5f016c32d..8ca4e78f8 100644 --- a/changelog.d/20250729_094933_sirosen_remove_filter_role.rst +++ b/changelog.d/20250729_094933_sirosen_remove_filter_role.rst @@ -2,4 +2,4 @@ Removed ------- - Removed the ``filter_role`` parameter to ``FlowsClient.list_flows``. - This parameter was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) + This parameter was deprecated in ``globus-sdk`` version 3. (:pr:`1291`) From f62891e94114d68faa39fcef441634c91d515ae7 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 29 Jul 2025 10:49:00 -0500 Subject: [PATCH 143/176] Remove SearchClient.update_entry (#1292) --- ...729_101404_sirosen_remove_update_entry.rst | 5 ++ src/globus_sdk/services/search/client.py | 47 ------------------- .../services/search/test_update_entry.py | 6 --- 3 files changed, 5 insertions(+), 53 deletions(-) create mode 100644 changelog.d/20250729_101404_sirosen_remove_update_entry.rst delete mode 100644 tests/functional/services/search/test_update_entry.py diff --git a/changelog.d/20250729_101404_sirosen_remove_update_entry.rst b/changelog.d/20250729_101404_sirosen_remove_update_entry.rst new file mode 100644 index 000000000..104d1a12b --- /dev/null +++ b/changelog.d/20250729_101404_sirosen_remove_update_entry.rst @@ -0,0 +1,5 @@ +Removed +------- + +- Removed ``SearchClient.update_entry``. + This method was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index 23f41f801..e9de4f74e 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -791,53 +791,6 @@ def create_entry( log.debug(f"SearchClient.create_entry({index_id}, ...)") return self.post(f"/v1/index/{index_id}/entry", data=data) - def update_entry( - self, index_id: uuid.UUID | str, data: dict[str, t.Any] - ) -> response.GlobusHTTPResponse: - """ - This API method is in effect an alias of ingest and is deprecated. - Users are recommended to use :meth:`~.ingest` instead. - - Create or update one Entry document in Search. - - This does not do a partial update, but replaces the existing document. - - :param index_id: the index containing this Entry - :param data: the entry document to write - - .. tab-set:: - - .. tab-item:: Example Usage - - Update an entry with a subject of ``https://example.com/foo/bar`` and - a null entry_id: - - .. code-block:: python - - sc = globus_sdk.SearchClient(...) - sc.update_entry( - index_id, - { - "subject": "https://example.com/foo/bar", - "visible_to": ["public"], - "content": {"foo/bar": "some val"}, - }, - ) - - .. tab-item:: API Info - - ``PUT /v1/index//entry`` - - .. extdoclink:: Update Entry - :ref: search/reference/create_or_update_entry/ - """ - warn_deprecated( - "SearchClient.update_entry is deprecated. " - "Users should prefer using `SearchClient.ingest`" - ) - log.debug(f"SearchClient.update_entry({index_id}, ...)") - return self.put(f"/v1/index/{index_id}/entry", data=data) - def delete_entry( self, index_id: uuid.UUID | str, diff --git a/tests/functional/services/search/test_update_entry.py b/tests/functional/services/search/test_update_entry.py deleted file mode 100644 index a6570cbc5..000000000 --- a/tests/functional/services/search/test_update_entry.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - - -@pytest.mark.xfail -def test_update_entry(): - raise NotImplementedError From 23d3f05fb04f94441222ab5e9d6c0900015a6533 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Jul 2025 15:49:16 +0000 Subject: [PATCH 144/176] (actions) update PR references --- changelog.d/20250729_101404_sirosen_remove_update_entry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250729_101404_sirosen_remove_update_entry.rst b/changelog.d/20250729_101404_sirosen_remove_update_entry.rst index 104d1a12b..85512f7a6 100644 --- a/changelog.d/20250729_101404_sirosen_remove_update_entry.rst +++ b/changelog.d/20250729_101404_sirosen_remove_update_entry.rst @@ -2,4 +2,4 @@ Removed ------- - Removed ``SearchClient.update_entry``. - This method was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) + This method was deprecated in ``globus-sdk`` version 3. (:pr:`1292`) From b73565f0f6493ecdc438a6cc119643a506a98992 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Jul 2025 16:25:20 +0000 Subject: [PATCH 145/176] (actions) update PR references --- changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst b/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst index 78ab5490c..1baac6d05 100644 --- a/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst +++ b/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst @@ -5,4 +5,4 @@ Changed ``globus_sdk.token_storage.legacy`` subpackage. Users are encouraged to migrate to the newer tooling available directly from - ``globus_sdk.token_storage``. (:pr:`NUMBER`) + ``globus_sdk.token_storage``. (:pr:`1290`) From 68098873fbbba2f89233a384089bdd535da4b3cd Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 29 Jul 2025 10:30:46 -0500 Subject: [PATCH 146/176] Remove the "SearchQuery" type Additionally, remove the base type which it depended upon and the `set_marker` helper on the scroll query type. This provides better uniformity between our query helper type and the scrolling query helper type. If users indicate that they found the setter methods useful, we can reintroduce them. --- ...3001_sirosen_remove_search_query_class.rst | 5 + docs/services/search.rst | 13 - src/globus_sdk/__init__.pyi | 2 - src/globus_sdk/services/search/__init__.py | 3 +- src/globus_sdk/services/search/client.py | 4 +- src/globus_sdk/services/search/data.py | 234 +----------------- .../functional/services/search/test_search.py | 56 +---- tests/unit/helpers/test_search.py | 197 +-------------- 8 files changed, 20 insertions(+), 494 deletions(-) create mode 100644 changelog.d/20250729_103001_sirosen_remove_search_query_class.rst diff --git a/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst b/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst new file mode 100644 index 000000000..b29d75718 --- /dev/null +++ b/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst @@ -0,0 +1,5 @@ +Removed +------- + +- Removed the ``SearchQuery`` type. Users should use ``SearchQueryV1`` instead. + ``SearchQuery`` was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) diff --git a/docs/services/search.rst b/docs/services/search.rst index 38146ac9e..d5d439773 100644 --- a/docs/services/search.rst +++ b/docs/services/search.rst @@ -12,19 +12,6 @@ Globus Search Helper Objects -------------- -Note that you should not use -:class:`SearchQueryBase ` directly, -and it is not importable from the top level of the SDK. It is included in documentation -only to document the methods it provides to its subclasses. - -.. autoclass:: globus_sdk.services.search.data.SearchQueryBase - :members: - :show-inheritance: - -.. autoclass:: SearchQuery - :members: - :show-inheritance: - .. autoclass:: SearchQueryV1 :members: :show-inheritance: diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index 8c40b762c..d134245ce 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -103,7 +103,6 @@ from .services.groups import ( from .services.search import ( SearchAPIError, SearchClient, - SearchQuery, SearchQueryV1, SearchScrollQuery, ) @@ -222,7 +221,6 @@ __all__ = ( "GroupVisibility", "SearchAPIError", "SearchClient", - "SearchQuery", "SearchQueryV1", "SearchScrollQuery", "OnceTimerSchedule", diff --git a/src/globus_sdk/services/search/__init__.py b/src/globus_sdk/services/search/__init__.py index 1084a4948..6f2eb2b90 100644 --- a/src/globus_sdk/services/search/__init__.py +++ b/src/globus_sdk/services/search/__init__.py @@ -1,10 +1,9 @@ from .client import SearchClient -from .data import SearchQuery, SearchQueryV1, SearchScrollQuery +from .data import SearchQueryV1, SearchScrollQuery from .errors import SearchAPIError __all__ = ( "SearchClient", - "SearchQuery", "SearchQueryV1", "SearchScrollQuery", "SearchAPIError", diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index e9de4f74e..73a6689e6 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -10,7 +10,7 @@ from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import SearchScopes -from .data import SearchQuery, SearchScrollQuery +from .data import SearchQueryV1, SearchScrollQuery from .errors import SearchAPIError from .response import IndexListResponse @@ -297,7 +297,7 @@ def search( def post_search( self, index_id: uuid.UUID | str, - data: dict[str, t.Any] | SearchQuery, + data: dict[str, t.Any] | SearchQueryV1, *, offset: int | MissingType = MISSING, limit: int | MissingType = MISSING, diff --git a/src/globus_sdk/services/search/data.py b/src/globus_sdk/services/search/data.py index d690f30ca..839b0df25 100644 --- a/src/globus_sdk/services/search/data.py +++ b/src/globus_sdk/services/search/data.py @@ -2,235 +2,14 @@ import typing as t -from globus_sdk import exc from globus_sdk._missing import MISSING, MissingType from globus_sdk._payload import GlobusPayload -# workaround for absence of Self type -# for the workaround and some background, see: -# https://github.com/python/mypy/issues/11871 -SearchQueryT = t.TypeVar("SearchQueryT", bound="SearchQueryBase") - - -def _format_histogram_range( - value: tuple[t.Any, t.Any] | MissingType, -) -> dict[str, t.Any] | MissingType: - if value is MISSING: - return MISSING - low, high = value - return {"low": low, "high": high} - - -# an internal class for declaring multiple related types with shared methods -class SearchQueryBase(GlobusPayload): - """ - The base class for all Search query helpers. - - Search has multiple types of query documents. Not all of their supported attributes - are shared, and they therefore do not inherit from one another. - This class implements common methods to all of them. - - Query objects have a chainable API, in which methods return the query object after - modification. This allows usage like - - >>> query = ... - >>> query = query.set_limit(10).set_advanced(False) - """ - - def set_query(self: SearchQueryT, query: str) -> SearchQueryT: - """ - Set the query string for the query document. - - :param query: the new query string - """ - self["q"] = query - return self - - def set_limit(self: SearchQueryT, limit: int) -> SearchQueryT: - """ - Set the limit for the query document. - - :param limit: a limit on the number of results returned in a single page - """ - self["limit"] = limit - return self - - def set_advanced(self: SearchQueryT, advanced: bool) -> SearchQueryT: - """ - Enable or disable advanced query string processing. - - :param advanced: whether to enable (``True``) or not (``False``) - """ - self["advanced"] = advanced - return self - - def add_filter( - self: SearchQueryT, - field_name: str, - values: list[str], - *, - # pylint: disable=redefined-builtin - type: str = "match_all", - additional_fields: dict[str, t.Any] | None = None, - ) -> SearchQueryT: - """ - Add a filter subdocument to the query. - - :param field_name: the field on which to filter - :param values: the values to use in the filter - :param type: the type of filter to apply, defaults to "match_all" - :param additional_fields: additional data to include in the filter document - """ - self["filters"] = self.get("filters", []) - new_filter = { - "field_name": field_name, - "values": values, - "type": type, - **(additional_fields or {}), - } - self["filters"].append(new_filter) - return self - - -class SearchQuery(SearchQueryBase): - """ - A specialized dict which has helpers for creating and modifying a Search - Query document. - - :param q: The query string. Required unless filters are used. - :param limit: A limit on the number of results returned in a single page - :param offset: An offset into the set of all results for the query - :param advanced: Whether to enable (``True``) or not to enable (``False``) advanced - parsing of query strings. The default of ``False`` is robust and guarantees that - the query will not error with "bad query string" errors - :param additional_fields: additional data to include in the query document - - Example usage: - - >>> from globus_sdk import SearchClient, SearchQuery - >>> sc = SearchClient(...) - >>> index_id = ... - >>> query = (SearchQuery(q='example query') - >>> .set_limit(100).set_offset(10) - >>> .add_filter('path.to.field1', ['foo', 'bar'])) - >>> result = sc.post_search(index_id, query) - """ - - def __init__( - self, - q: str | MissingType = MISSING, - *, - limit: int | MissingType = MISSING, - offset: int | MissingType = MISSING, - advanced: bool | MissingType = MISSING, - additional_fields: dict[str, t.Any] | None = None, - ) -> None: - super().__init__() - exc.warn_deprecated("'SearchQuery' is deprecated. Use 'SearchQueryV1' instead.") - - self["q"] = q - self["limit"] = limit - self["offset"] = offset - self["advanced"] = advanced - self.update(additional_fields or {}) - - def set_offset(self, offset: int) -> SearchQuery: - """ - Set the offset for the query document. - - :param offset: an offset into the set of all results for the query - """ - self["offset"] = offset - return self - - def add_facet( - self, - name: str, - field_name: str, - *, - # pylint: disable=redefined-builtin - type: str = "terms", - size: int | MissingType = MISSING, - date_interval: str | MissingType = MISSING, - histogram_range: tuple[t.Any, t.Any] | MissingType = MISSING, - additional_fields: dict[str, t.Any] | None = None, - ) -> SearchQuery: - """ - Add a facet subdocument to the query. - - :param name: the name for the facet in the result - :param field_name: the field on which to build the facet - :param type: the type of facet to apply, defaults to "terms" - :param size: the size parameter for the facet - :param date_interval: the date interval for a date histogram facet - :param histogram_range: a low and high bound for a numeric histogram facet - :param additional_fields: additional data to include in the facet document - """ - self["facets"] = self.get("facets", []) - facet: dict[str, t.Any] = { - "name": name, - "field_name": field_name, - "type": type, - "size": size, - "date_interval": date_interval, - "histogram_range": _format_histogram_range(histogram_range), - **(additional_fields or {}), - } - self["facets"].append(facet) - return self - - def add_boost( - self, - field_name: str, - factor: str | int | float, - *, - additional_fields: dict[str, t.Any] | None = None, - ) -> SearchQuery: - """ - Add a boost subdocument to the query. - - :param field_name: the field to boost in result weighting - :param factor: the factor by which to adjust the field weight (where ``1.0`` is - the default weight) - :param additional_fields: additional data to include in the boost document - """ - self["boosts"] = self.get("boosts", []) - boost = { - "field_name": field_name, - "factor": factor, - **(additional_fields or {}), - } - self["boosts"].append(boost) - return self - - def add_sort( - self, - field_name: str, - *, - order: str | MissingType = MISSING, - additional_fields: dict[str, t.Any] | None = None, - ) -> SearchQuery: - """ - Add a sort subdocument to the query. - - :param field_name: the field on which to sort - :param order: ascending or descending order, given as ``"asc"`` or ``"desc"`` - :param additional_fields: additional data to include in the sort document - """ - self["sort"] = self.get("sort", []) - sort = { - "field_name": field_name, - "order": order, - **(additional_fields or {}), - } - self["sort"].append(sort) - return self - class SearchQueryV1(GlobusPayload): """ A specialized dict which has helpers for creating and modifying a Search - Query document. Replaces the usage of ``SearchQuery``. + Query document. :param q: The query string. Required unless filters are used. :param limit: A limit on the number of results returned in a single page @@ -276,7 +55,7 @@ def __init__( self.update(additional_fields or {}) -class SearchScrollQuery(SearchQueryBase): +class SearchScrollQuery(GlobusPayload): """ A scrolling query type, for scrolling the full result set for an index. @@ -311,12 +90,3 @@ def __init__( self["advanced"] = advanced self["marker"] = marker self.update(additional_fields or {}) - - def set_marker(self, marker: str) -> SearchScrollQuery: - """ - Set the marker on a scroll query. - - :param marker: the marker value - """ - self["marker"] = marker - return self diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index 4e5f6f5d3..97043bc8f 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -52,26 +52,6 @@ def test_search_post_query_simple(search_client, query_doc): assert req_body == dict(query_doc) -def test_search_post_query_with_legacy_helper(search_client): - meta = load_response(search_client.post_search).metadata - with pytest.warns( - globus_sdk.RemovedInV4Warning, match="'SearchQuery' is deprecated" - ): - query_doc = globus_sdk.SearchQuery("foo") - - res = search_client.post_search(meta["index_id"], query_doc) - assert res.http_status == 200 - - data = res.data - assert isinstance(data, dict) - assert data["gmeta"][0]["entries"][0]["content"]["foo"] == "bar" - - req = get_last_request() - assert req.body is not None - req_body = json.loads(req.body) - assert req_body == filter_missing(query_doc) - - def test_search_post_query_simple_with_v1_helper(search_client): query_doc = globus_sdk.SearchQueryV1(q="foo") meta = load_response(search_client.post_search).metadata @@ -89,36 +69,16 @@ def test_search_post_query_simple_with_v1_helper(search_client): assert req_body == {"@version": "query#1.0.0", "q": "foo"} -def test_search_post_query_arg_overrides(search_client): - meta = load_response(search_client.post_search).metadata - - query_doc = {"q": "foo", "limit": 10, "offset": 0} - res = search_client.post_search(meta["index_id"], query_doc, limit=100, offset=150) - assert res.http_status == 200 - - data = res.data - assert isinstance(data, dict) - assert data["gmeta"][0]["entries"][0]["content"]["foo"] == "bar" - - req = get_last_request() - assert req.body is not None - req_body = json.loads(req.body) - assert req_body != dict(query_doc) - assert req_body["q"] == query_doc["q"] - assert req_body["limit"] == 100 - assert req_body["offset"] == 150 - # important! these should be unchanged (no side-effects) - assert query_doc["limit"] == 10 - assert query_doc["offset"] == 0 - - -def test_search_post_query_arg_overrides_with_legacy_helper(search_client): +@pytest.mark.parametrize("doc_type", ("dict", "helper")) +def test_search_post_query_arg_overrides(search_client, doc_type): meta = load_response(search_client.post_search).metadata - with pytest.warns( - globus_sdk.RemovedInV4Warning, match="'SearchQuery' is deprecated" - ): - query_doc = globus_sdk.SearchQuery("foo", limit=10, offset=0) + if doc_type == "dict": + query_doc = {"q": "foo", "limit": 10, "offset": 0} + elif doc_type == "helper": + query_doc = globus_sdk.SearchQueryV1(q="foo", limit=10, offset=0) + else: + raise NotImplementedError(doc_type) res = search_client.post_search(meta["index_id"], query_doc, limit=100, offset=150) assert res.http_status == 200 diff --git a/tests/unit/helpers/test_search.py b/tests/unit/helpers/test_search.py index 52b150654..b46409629 100644 --- a/tests/unit/helpers/test_search.py +++ b/tests/unit/helpers/test_search.py @@ -1,42 +1,8 @@ """ -Unit tests for globus_sdk.SearchQuery +Unit tests for globus_sdk.SearchQueryV1 """ -import pytest - -from globus_sdk import MISSING, RemovedInV4Warning, SearchQuery, SearchQueryV1 -from globus_sdk._missing import filter_missing - - -def test_init_legacy(): - params = {"q": "foo", "limit": 10, "offset": 0, "advanced": False} - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - param_query = SearchQuery(**params) - for par in params: - assert param_query[par] == params[par] - - -def test_init_legacy_no_args(): - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - query = SearchQuery() - - assert len(filter_missing(query)) == 0 - - -def test_init_legacy_additional_fields(): - add_params = {"param1": "value1", "param2": "value2"} - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - param_query = SearchQuery(additional_fields=add_params) - for par in add_params: - assert param_query[par] == add_params[par] - - -def test_init_legacy_deprecation_warning(): - with pytest.warns( - RemovedInV4Warning, - match="'SearchQuery' is deprecated. Use 'SearchQueryV1' instead.", - ): - SearchQuery() +from globus_sdk import MISSING, SearchQueryV1 def test_init_v1(): @@ -60,162 +26,3 @@ def test_init_v1(): param_query = SearchQueryV1(additional_fields=add_params) for par in add_params: assert param_query[par] == add_params[par] - - -@pytest.mark.parametrize("attrname", ["q", "limit", "offset", "advanced"]) -def test_set_method(attrname): - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - query = SearchQuery() - method = getattr(query, "set_{}".format("query" if attrname == "q" else attrname)) - # start absent - assert attrname not in filter_missing(query) - # returns self - assert method("foo") is query - # sets value - assert query[attrname] == "foo" - - -def test_add_facet(): - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - query = SearchQuery() - assert "facets" not in query - - # simple terms facet - # returns self - assert query.add_facet("facetname", "fieldname") is query - assert query["facets"] - assert len(query["facets"]) == 1 - assert filter_missing(query["facets"][0]) == { - "type": "terms", - "name": "facetname", - "field_name": "fieldname", - } - - # terms with size - query.add_facet("n", "f", size=5) - assert len(query["facets"]) == 2 - assert filter_missing(query["facets"][1]) == { - "type": "terms", - "name": "n", - "field_name": "f", - "size": 5, - } - - # date histogram - query.add_facet( - "n", - "f", - type="date_histogram", - date_interval="year", - histogram_range=(1870, 1880), - ) - assert len(query["facets"]) == 3 - assert filter_missing(query["facets"][2]) == { - "type": "date_histogram", - "name": "n", - "field_name": "f", - "date_interval": "year", - "histogram_range": {"low": 1870, "high": 1880}, - } - - # unknown param - query.add_facet( - "facetname", "fieldname", additional_fields={"nonexistentparam": "value1"} - ) - assert len(query["facets"]) == 4 - assert filter_missing(query["facets"][3]) == { - "type": "terms", - "name": "facetname", - "field_name": "fieldname", - "nonexistentparam": "value1", - } - - -def test_add_filter(): - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - query = SearchQuery() - assert "filters" not in query - - # returns self - assert query.add_filter("f", [1, 2, 3]) is query - - assert query["filters"] - assert len(query["filters"]) == 1 - assert query["filters"][0] == { - "field_name": "f", - "type": "match_all", - "values": [1, 2, 3], - } - - # match_any + custom param - query.add_filter( - "f", - [1, 2, 3], - type="match_any", - additional_fields={"nonexistentparam": "val1"}, - ) - assert len(query["filters"]) == 2 - assert query["filters"][1] == { - "field_name": "f", - "type": "match_any", - "values": [1, 2, 3], - "nonexistentparam": "val1", - } - - # range - query.add_filter("f", [{"from": 1, "to": 100}], type="range") - assert len(query["filters"]) == 3 - assert query["filters"][2] == { - "field_name": "f", - "type": "range", - "values": [{"from": 1, "to": 100}], - } - - -def test_add_boost(): - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - query = SearchQuery() - assert "boosts" not in query - - # returns self - assert query.add_boost("f", 2) == query - - assert query["boosts"] - assert len(query["boosts"]) == 1 - assert query["boosts"][0] == {"field_name": "f", "factor": 2} - - # custom param - query.add_boost("f", 1.1, additional_fields={"nonexistentparam": "value1"}) - assert len(query["boosts"]) == 2 - assert query["boosts"][1] == { - "field_name": "f", - "factor": 1.1, - "nonexistentparam": "value1", - } - - -def test_add_sort(): - with pytest.warns(RemovedInV4Warning, match="'SearchQuery' is deprecated"): - query = SearchQuery() - assert "sort" not in query - - # returns self - assert query.add_sort("f") is query - - assert query["sort"] - assert len(query["sort"]) == 1 - assert filter_missing(query["sort"][0]) == {"field_name": "f"} - - # with order - query.add_sort("f", order="asc") - assert len(query["sort"]) == 2 - assert query["sort"][1] == {"field_name": "f", "order": "asc"} - - # custom param - query.add_sort("f", order="asc", additional_fields={"nonexistentparam": "value1"}) - assert len(query["sort"]) == 3 - assert query["sort"][2] == { - "field_name": "f", - "order": "asc", - "nonexistentparam": "value1", - } From f2449c451ae884769d8d60a7f1928535768336b2 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 29 Jul 2025 12:03:13 -0500 Subject: [PATCH 147/176] Add upgrading doc for 'SearchQuery' --- docs/upgrading.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index d9da53640..990534f55 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -331,6 +331,58 @@ The removed alias and new module names are shown in the table below. "``globus_sdk.experimental.tokenstorage``", "``globus_sdk.token_storage``" "``globus_sdk.experimental.login_flow_manager``", "``globus_sdk.login_flows``" +``SearchQuery`` is Removed, use ``SearchQueryV1`` Instead +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``SearchQuery`` helper was removed in version 4 in favor of the +:class:`SearchQueryV1 ` type. + +Simply replace one type with the other for most simple usages: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk import SearchQuery + + query = SearchQuery(q="foo") + + # globus-sdk v4 + from globus_sdk import SearchQuery + + query = SearchQueryV1(q="foo") + +Note that ``SearchQuery`` supported the query string, ``q``, as a positional +argument, but ``SearchQueryV1`` requires that it is passed as a named +parameter. + +``SearchQuery`` also supported helper methods which are not provided by +``SearchQueryV1``. +These must be replaced by setting the relevant parameters directly or on +initialization. +For example: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk import SearchQuery + + query = SearchQuery(q="foo") + query.set_offset(100) # removed in v4 + + # globus-sdk v4 + from globus_sdk import SearchQuery + + query = SearchQueryV1(q="foo", offset=100) # on init + # or + query = SearchQueryV1(q="foo") + query["offset"] = 100 # by setting a field + +.. note:: + + :class:`SearchQueryV1 ` was added in + ``globus-sdk`` version 3, so this transition can be made prior to upgrading + to version 4. + ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 26fd489b9efb22c89dabb03d61139bdedd5ecb2e Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Jul 2025 17:11:11 +0000 Subject: [PATCH 148/176] (actions) update PR references --- .../20250725_172504_sirosen_cleanup_scope_normalization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst b/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst index b61e39621..f788fb3b4 100644 --- a/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst +++ b/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst @@ -1,7 +1,7 @@ Breaking Changes ---------------- -- Interfaces for normalizing scope data have changed. (:pr:`NUMBER`) +- Interfaces for normalizing scope data have changed. (:pr:`1289`) - The ``scopes_to_str`` function has been replaced with ``ScopeParser.serialize``. From e61a38b803604d37cb4607d82f667d8ac667fc24 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Jul 2025 20:48:41 +0000 Subject: [PATCH 149/176] (actions) update PR references --- .../20250729_103001_sirosen_remove_search_query_class.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst b/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst index b29d75718..4f1d776ee 100644 --- a/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst +++ b/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst @@ -2,4 +2,4 @@ Removed ------- - Removed the ``SearchQuery`` type. Users should use ``SearchQueryV1`` instead. - ``SearchQuery`` was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) + ``SearchQuery`` was deprecated in ``globus-sdk`` version 3. (:pr:`1294`) From cd634932820d67bf6fbd86f0217dd5221400d980 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 29 Jul 2025 10:16:44 -0500 Subject: [PATCH 150/176] Remove SearchClient.create_entry --- ...729_101626_sirosen_remove_create_entry.rst | 5 ++ src/globus_sdk/services/search/client.py | 65 ------------------- .../services/search/test_create_entry.py | 6 -- 3 files changed, 5 insertions(+), 71 deletions(-) create mode 100644 changelog.d/20250729_101626_sirosen_remove_create_entry.rst delete mode 100644 tests/functional/services/search/test_create_entry.py diff --git a/changelog.d/20250729_101626_sirosen_remove_create_entry.rst b/changelog.d/20250729_101626_sirosen_remove_create_entry.rst new file mode 100644 index 000000000..0056946e0 --- /dev/null +++ b/changelog.d/20250729_101626_sirosen_remove_create_entry.rst @@ -0,0 +1,5 @@ +Removed +------- + +- Removed ``SearchClient.create_entry``. + This method was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/search/client.py b/src/globus_sdk/services/search/client.py index 73a6689e6..c42c27483 100644 --- a/src/globus_sdk/services/search/client.py +++ b/src/globus_sdk/services/search/client.py @@ -7,7 +7,6 @@ from globus_sdk import client, paging, response from globus_sdk._internal.remarshal import strseq_listify from globus_sdk._missing import MISSING, MissingType -from globus_sdk.exc.warnings import warn_deprecated from globus_sdk.scopes import SearchScopes from .data import SearchQueryV1, SearchScrollQuery @@ -727,70 +726,6 @@ def get_entry( } return self.get(f"/v1/index/{index_id}/entry", query_params=query_params) - def create_entry( - self, index_id: uuid.UUID | str, data: dict[str, t.Any] - ) -> response.GlobusHTTPResponse: - """ - This API method is in effect an alias of ingest and is deprecated. - Users are recommended to use :meth:`~.ingest` instead. - - Create or update one Entry document in Search. - - The API does not enforce that the document does not exist, and will overwrite - any existing data. - - :param index_id: the index containing this Entry - :param data: the entry document to write - - .. tab-set:: - - .. tab-item:: Example Usage - - Create an entry with a subject of ``https://example.com/foo/bar`` and - a null entry_id: - - .. code-block:: python - - sc = globus_sdk.SearchClient(...) - sc.create_entry( - index_id, - { - "subject": "https://example.com/foo/bar", - "visible_to": ["public"], - "content": {"foo/bar": "some val"}, - }, - ) - - Create an entry with a subject of ``https://example.com/foo/bar`` and - an entry_id of ``foo/bar``: - - .. code-block:: python - - sc = globus_sdk.SearchClient(...) - sc.create_entry( - index_id, - { - "subject": "https://example.com/foo/bar", - "visible_to": ["public"], - "id": "foo/bar", - "content": {"foo/bar": "some val"}, - }, - ) - - .. tab-item:: API Info - - ``POST /v1/index//entry`` - - .. extdoclink:: Create Entry - :ref: search/reference/create_or_update_entry/ - """ - warn_deprecated( - "SearchClient.create_entry is deprecated. " - "Users should prefer using `SearchClient.ingest`" - ) - log.debug(f"SearchClient.create_entry({index_id}, ...)") - return self.post(f"/v1/index/{index_id}/entry", data=data) - def delete_entry( self, index_id: uuid.UUID | str, diff --git a/tests/functional/services/search/test_create_entry.py b/tests/functional/services/search/test_create_entry.py deleted file mode 100644 index d00bfa873..000000000 --- a/tests/functional/services/search/test_create_entry.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - - -@pytest.mark.xfail -def test_create_entry(): - raise NotImplementedError From b85c0f0eacbbb5968197915983f500aeffd3eaac Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 29 Jul 2025 12:08:58 -0500 Subject: [PATCH 151/176] Add a small section to upgrading doc for 'ingest' --- docs/upgrading.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 990534f55..fac5b9e22 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -383,6 +383,17 @@ For example: ``globus-sdk`` version 3, so this transition can be made prior to upgrading to version 4. +``SearchClient.create_entry`` and ``SearchClient.update_entry`` Removed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These methods were deprecated in version 3 in favor of ``SearchClient.ingest``, +which provides greater functionality and a more uniform interface. + +For any document being passed by these methods, upgrade to using an ingest +document with ``"ingest_type": "GMetaEntry"``. +Consult the :extdoclink:`Search Ingest Guide ` +for details on the document formats. + ``MutableScope`` is Removed, use ``Scope`` Instead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From a5726ee0b18d046a7e82abbd2007a6abe08317e9 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 21 Jul 2025 15:50:06 -0500 Subject: [PATCH 152/176] Change how `BaseClient.transport` is customized Allow users to pass in a constructed transport, rather than always requiring that they pass parameters which the client will then use to instantiate a new transport. This enables experimentation with transport reuse, sharing, and other paradigms of use which were previously not naturally possible. It also reduces a typing-time imprecision in the client initialization API, where the transport parameters are expected to match the transport class, but this cannot be enforced contractually because the class itself is dynamic. Rather than providing a `transport_class`, clients now have a `default_transport_factory`. This is typically going to be a class, but conceptually it's any callable with no arguments. --- docs/upgrading.rst | 69 +++++++++++++++++++ src/globus_sdk/client.py | 14 ++-- .../services/auth/client/base_login_client.py | 5 +- .../auth/client/confidential_client.py | 5 +- .../services/auth/client/native_client.py | 5 +- .../services/auth/client/service_client.py | 5 +- src/globus_sdk/services/flows/client.py | 5 +- src/globus_sdk/services/gcs/client.py | 5 +- src/globus_sdk/services/transfer/client.py | 4 +- .../auth/confidential_client/conftest.py | 2 +- tests/functional/services/auth/conftest.py | 4 +- .../services/auth/native_client/conftest.py | 2 +- .../services/auth/test_auth_client_flow.py | 4 +- tests/functional/services/compute/conftest.py | 4 +- tests/functional/services/flows/conftest.py | 4 +- tests/functional/services/gcs/conftest.py | 2 +- tests/functional/services/groups/conftest.py | 2 +- tests/functional/services/search/conftest.py | 2 +- .../functional/services/search/test_search.py | 2 +- .../services/search/test_search_roles.py | 2 +- .../functional/services/transfer/conftest.py | 2 +- tests/functional/tokenstorage/v2/conftest.py | 2 +- .../sphinxext/test_copyparams_directive.py | 2 +- tests/unit/test_base_client.py | 7 +- 24 files changed, 121 insertions(+), 39 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 990534f55..01c8e395a 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -442,6 +442,75 @@ 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 both been replaced. +Client initialization now accepts a fully instantiated ``RequestsTransport`` +object, instead of ``transport_params``. +And client classes now define a ``default_transport_factory`` attribute, which is a +callable which returns a transport object. + +For users who are customizing the parameters to the transport class, either 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 v3 + import globus_sdk + + client = globus_sdk.GroupsClient(transport_params={"http_timeout": 120.0}) + my_groups = client.get_my_groups() + + # globus-sdk v4 + import globus_sdk + + client = globus_sdk.GroupsClient() + with client.transport.tune(http_timeout=120.0): + my_groups = client.get_my_groups() + +For users who are customizing the transport class for a custom client, update like so: + +.. code-block:: python + + # globus-sdk v3 + import globus_sdk + from .mylibrary import CustomTransport + + + class MyClient(globus_sdk.GroupsClient): + transport_class = CustomTransport + + + # globus-sdk v4 + import globus_sdk + from .mylibrary import CustomTransport + + + class MyClient(globus_sdk.GroupsClient): + default_transport_factory = CustomTransport + From 1.x or 2.x to 3.0 ----------------------- diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 9f441d6c7..53dcb5b23 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -48,7 +48,8 @@ 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 + :param transport: A :class:`RequestsTransport` object for sending and + retrying requests. By default, one will be constructed by the client. All other parameters are for internal use and should be ignored. """ @@ -65,8 +66,9 @@ 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 + #: a function or class which will be used to construct a transport if one + #: is not provided, defaults to ``RequestsTransport`` + default_transport_factory: t.Callable[[], RequestsTransport] = RequestsTransport #: the scopes for this client may be present as a ``ScopeCollection`` scopes: ScopeCollection | None = None @@ -80,7 +82,7 @@ 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, ) -> None: # check for input parameter conflicts if app_scopes and not app: @@ -107,7 +109,9 @@ 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.transport = ( + transport if transport is not None else self.default_transport_factory() + ) log.debug(f"initialized transport of type {type(self.transport)}") # setup paginated methods diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index ab86cd661..65a86bb48 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -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 from .._common import get_jwk_data, pem_decode_jwk_data from ..errors import AuthAPIError @@ -52,14 +53,14 @@ 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, ) -> None: super().__init__( environment=environment, base_url=base_url, authorizer=authorizer, app_name=app_name, - transport_params=transport_params, + transport=transport, ) 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 diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 4783a71c4..41c35627c 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -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 from ..flow_managers import GlobusAuthorizationCodeFlowManager from ..response import OAuthClientCredentialsResponse, OAuthDependentTokenResponse @@ -47,7 +48,7 @@ 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, ) -> None: super().__init__( client_id=client_id, @@ -55,7 +56,7 @@ def __init__( environment=environment, base_url=base_url, app_name=app_name, - transport_params=transport_params, + transport=transport, ) def oauth2_client_credentials_tokens( diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index d801f3b69..364413534 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -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 from ..flow_managers import GlobusNativeAppFlowManager from ..response import OAuthRefreshTokenResponse @@ -39,7 +40,7 @@ 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, ) -> None: super().__init__( client_id=client_id, @@ -47,7 +48,7 @@ def __init__( environment=environment, base_url=base_url, app_name=app_name, - transport_params=transport_params, + transport=transport, ) def oauth2_start_flow( diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index b927ce60f..ed3c3b633 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -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 if t.TYPE_CHECKING: from globus_sdk.globus_app import GlobusApp @@ -78,7 +79,7 @@ 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, ) -> None: super().__init__( environment=environment, @@ -87,7 +88,7 @@ def __init__( app_scopes=app_scopes, authorizer=authorizer, app_name=app_name, - transport_params=transport_params, + transport=transport, ) # FYI: this get_openid_configuration method is duplicated in AuthLoginBaseClient diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 8943de904..864a657e5 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -18,6 +18,7 @@ SpecificFlowScopes, TransferScopes, ) +from globus_sdk.transport import RequestsTransport from .data import RunActivityNotificationPolicy from .errors import FlowsAPIError @@ -908,7 +909,7 @@ 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, ) -> None: self._flow_id = flow_id self.scopes = SpecificFlowScopes(flow_id) @@ -918,7 +919,7 @@ def __init__( environment=environment, authorizer=authorizer, app_name=app_name, - transport_params=transport_params, + transport=transport, ) @property diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index c05a1cfe7..e6d43399d 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -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 from .data import ( CollectionDocument, @@ -56,7 +57,7 @@ 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, ) -> None: # check if the provided address was a DNS name or an HTTPS URL if not gcs_address.startswith("https://"): @@ -76,7 +77,7 @@ def __init__( app_scopes=app_scopes, authorizer=authorizer, app_name=app_name, - transport_params=transport_params, + transport=transport, ) @staticmethod diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index edc8b11a6..c877173b5 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -129,7 +129,9 @@ class TransferClient(client.BaseClient): """ service_name = "transfer" - transport_class: type[TransferRequestsTransport] = TransferRequestsTransport + default_transport_factory: t.Callable[[], TransferRequestsTransport] = ( + TransferRequestsTransport + ) error_class = TransferAPIError scopes = TransferScopes default_scope_requirements = [TransferScopes.all] diff --git a/tests/functional/services/auth/confidential_client/conftest.py b/tests/functional/services/auth/confidential_client/conftest.py index c59a40f1a..0653e7b83 100644 --- a/tests/functional/services/auth/confidential_client/conftest.py +++ b/tests/functional/services/auth/confidential_client/conftest.py @@ -6,6 +6,6 @@ @pytest.fixture def auth_client(no_retry_transport): class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomAuthClient("dummy_client_id", "dummy_client_secret") diff --git a/tests/functional/services/auth/conftest.py b/tests/functional/services/auth/conftest.py index 1153551ea..c6071ad37 100644 --- a/tests/functional/services/auth/conftest.py +++ b/tests/functional/services/auth/conftest.py @@ -6,7 +6,7 @@ @pytest.fixture def login_client(no_retry_transport): class CustomAuthClient(globus_sdk.AuthLoginClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomAuthClient() @@ -14,6 +14,6 @@ class CustomAuthClient(globus_sdk.AuthLoginClient): @pytest.fixture def service_client(no_retry_transport): class CustomAuthClient(globus_sdk.AuthClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomAuthClient() diff --git a/tests/functional/services/auth/native_client/conftest.py b/tests/functional/services/auth/native_client/conftest.py index 78c4d6417..0d9168d6f 100644 --- a/tests/functional/services/auth/native_client/conftest.py +++ b/tests/functional/services/auth/native_client/conftest.py @@ -6,6 +6,6 @@ @pytest.fixture def auth_client(no_retry_transport): class CustomAuthClient(globus_sdk.NativeAppAuthClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomAuthClient("dummy_client_id") diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index c0549dfdd..7bc96aedd 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -15,7 +15,7 @@ @pytest.fixture def native_client(no_retry_transport): class CustomAuthClient(globus_sdk.NativeAppAuthClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomAuthClient(client_id=CLIENT_ID) @@ -23,7 +23,7 @@ class CustomAuthClient(globus_sdk.NativeAppAuthClient): @pytest.fixture def confidential_client(no_retry_transport): class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomAuthClient( client_id=CLIENT_ID, client_secret="SECRET_SECRET_HES_GOT_A_SECRET" diff --git a/tests/functional/services/compute/conftest.py b/tests/functional/services/compute/conftest.py index 8264cd0dc..4755a7f71 100644 --- a/tests/functional/services/compute/conftest.py +++ b/tests/functional/services/compute/conftest.py @@ -6,7 +6,7 @@ @pytest.fixture def compute_client_v2(no_retry_transport): class CustomComputeClientV2(globus_sdk.ComputeClientV2): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomComputeClientV2() @@ -14,6 +14,6 @@ class CustomComputeClientV2(globus_sdk.ComputeClientV2): @pytest.fixture def compute_client_v3(no_retry_transport): class CustomComputeClientV3(globus_sdk.ComputeClientV3): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomComputeClientV3() diff --git a/tests/functional/services/flows/conftest.py b/tests/functional/services/flows/conftest.py index 0bdd788d9..ea982f41e 100644 --- a/tests/functional/services/flows/conftest.py +++ b/tests/functional/services/flows/conftest.py @@ -8,7 +8,7 @@ @pytest.fixture def flows_client(no_retry_transport): class CustomFlowsClient(globus_sdk.FlowsClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomFlowsClient() @@ -18,6 +18,6 @@ def specific_flow_client_class( no_retry_transport, ) -> t.Type[globus_sdk.SpecificFlowClient]: class CustomSpecificFlowClient(globus_sdk.SpecificFlowClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomSpecificFlowClient diff --git a/tests/functional/services/gcs/conftest.py b/tests/functional/services/gcs/conftest.py index 8a098e5f6..b4743c4fe 100644 --- a/tests/functional/services/gcs/conftest.py +++ b/tests/functional/services/gcs/conftest.py @@ -6,7 +6,7 @@ @pytest.fixture def client(no_retry_transport): class CustomGCSClient(GCSClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport # default fqdn for GCS client testing return CustomGCSClient("abc.xyz.data.globus.org") diff --git a/tests/functional/services/groups/conftest.py b/tests/functional/services/groups/conftest.py index 597dda417..9c2c1c7ff 100644 --- a/tests/functional/services/groups/conftest.py +++ b/tests/functional/services/groups/conftest.py @@ -6,7 +6,7 @@ @pytest.fixture def groups_client(no_retry_transport): class CustomGroupsClient(globus_sdk.GroupsClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomGroupsClient() diff --git a/tests/functional/services/search/conftest.py b/tests/functional/services/search/conftest.py index 6a0a8ac2a..ae3e84649 100644 --- a/tests/functional/services/search/conftest.py +++ b/tests/functional/services/search/conftest.py @@ -6,6 +6,6 @@ @pytest.fixture def client(no_retry_transport): class CustomSearchClient(globus_sdk.SearchClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomSearchClient() diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index 97043bc8f..e09e63d68 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -14,7 +14,7 @@ @pytest.fixture def search_client(no_retry_transport): class CustomSearchClient(globus_sdk.SearchClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomSearchClient() diff --git a/tests/functional/services/search/test_search_roles.py b/tests/functional/services/search/test_search_roles.py index 5efa4a80b..51e6ded10 100644 --- a/tests/functional/services/search/test_search_roles.py +++ b/tests/functional/services/search/test_search_roles.py @@ -9,7 +9,7 @@ @pytest.fixture def search_client(no_retry_transport): class CustomSearchClient(globus_sdk.SearchClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomSearchClient() diff --git a/tests/functional/services/transfer/conftest.py b/tests/functional/services/transfer/conftest.py index 6657c1ad2..b309c233a 100644 --- a/tests/functional/services/transfer/conftest.py +++ b/tests/functional/services/transfer/conftest.py @@ -6,6 +6,6 @@ @pytest.fixture def client(no_retry_transport): class CustomTransferClient(globus_sdk.TransferClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomTransferClient() diff --git a/tests/functional/tokenstorage/v2/conftest.py b/tests/functional/tokenstorage/v2/conftest.py index ed7acb4e9..7c46341c6 100644 --- a/tests/functional/tokenstorage/v2/conftest.py +++ b/tests/functional/tokenstorage/v2/conftest.py @@ -17,7 +17,7 @@ def id_token_sub(): @pytest.fixture def cc_auth_client(no_retry_transport): class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): - transport_class = no_retry_transport + default_transport_factory = no_retry_transport return CustomAuthClient("dummy_id", "dummy_secret") diff --git a/tests/unit/sphinxext/test_copyparams_directive.py b/tests/unit/sphinxext/test_copyparams_directive.py index 8bddd49a7..8dedb80ce 100644 --- a/tests/unit/sphinxext/test_copyparams_directive.py +++ b/tests/unit/sphinxext/test_copyparams_directive.py @@ -11,7 +11,7 @@ "authorizer", "app_name", "base_url", - "transport_params", + "transport", ) diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index df1abcd5e..81394cb53 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -11,6 +11,7 @@ from globus_sdk.scopes import Scope, TransferScopes from globus_sdk.testing import RegisteredResponse, get_last_request from globus_sdk.token_storage import TokenValidationError +from globus_sdk.transport import RequestsTransport @pytest.fixture @@ -22,7 +23,7 @@ def auth_client(): def base_client_class(no_retry_transport): class CustomClient(globus_sdk.BaseClient): service_name = "transfer" - transport_class = no_retry_transport + default_transport_factory = no_retry_transport scopes = TransferScopes default_scope_requirements = [TransferScopes.all] @@ -89,10 +90,10 @@ class FooClient(globus_sdk.BaseClient): client = FooClient() assert client.transport.http_timeout == 60.0 - client = FooClient(transport_params={"http_timeout": None}) + client = FooClient(transport=RequestsTransport(http_timeout=None)) assert client.transport.http_timeout == 60.0 - client = FooClient(transport_params={"http_timeout": -1}) + client = FooClient(transport=RequestsTransport(http_timeout=-1)) assert client.transport.http_timeout is None os.environ["GLOBUS_SDK_HTTP_TIMEOUT"] = "120" From 78e4c06f225d9ea305f6c636ddbc5f7280743a5f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 21 Jul 2025 18:35:19 -0500 Subject: [PATCH 153/176] Introduce RetryCheckCollection This replaces transport object factories. Instead, each client simply - has a transport (which the user may pass) - has a retry check collection (which is user-accessible but may not be passed on init) The retry check collection is the list-of-checks part of the transport, abstracted out into a separate type. It is passed into the transport as part of the caller info. This simplifies the transport itself, and makes each client independently responsible for its retry checks. As a result, the Transfer client can use the base transport type without issue. --- docs/upgrading.rst | 64 ++++++-- src/globus_sdk/client.py | 35 +++-- src/globus_sdk/services/transfer/client.py | 9 +- src/globus_sdk/services/transfer/transport.py | 16 +- src/globus_sdk/transport/__init__.py | 7 +- src/globus_sdk/transport/caller_info.py | 23 +++ .../transport/default_retry_checks.py | 119 ++++++++++++++ src/globus_sdk/transport/requests.py | 145 +----------------- src/globus_sdk/transport/retry.py | 41 ++++- tests/conftest.py | 5 +- .../base_client/test_retry_behavior.py | 4 +- .../auth/confidential_client/conftest.py | 7 +- tests/functional/services/auth/conftest.py | 10 +- .../services/auth/native_client/conftest.py | 7 +- .../services/auth/test_auth_client_flow.py | 16 +- tests/functional/services/compute/conftest.py | 10 +- tests/functional/services/flows/conftest.py | 9 +- tests/functional/services/gcs/conftest.py | 7 +- tests/functional/services/groups/conftest.py | 5 +- tests/functional/services/search/conftest.py | 5 +- .../functional/services/search/test_search.py | 5 +- .../services/search/test_search_roles.py | 5 +- .../functional/services/transfer/conftest.py | 5 +- tests/functional/tokenstorage/v2/conftest.py | 7 +- tests/unit/test_base_client.py | 5 +- .../transport/test_default_retry_policy.py | 42 ++--- .../unit/transport/test_retry_check_runner.py | 3 +- .../unit/transport/test_transfer_transport.py | 20 +-- .../test_transport_authz_handling.py | 24 ++- 29 files changed, 354 insertions(+), 306 deletions(-) create mode 100644 src/globus_sdk/transport/caller_info.py create mode 100644 src/globus_sdk/transport/default_retry_checks.py diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 01c8e395a..7641300ad 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -451,14 +451,11 @@ 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 both been replaced. -Client initialization now accepts a fully instantiated ``RequestsTransport`` -object, instead of ``transport_params``. -And client classes now define a ``default_transport_factory`` attribute, which is a -callable which returns a transport object. +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, either explicitly -instantiate the transport object: +For users who are customizing the parameters to the transport class, they +should now explicitly instantiate the transport object: .. code-block:: python @@ -490,26 +487,65 @@ or use the ``tune()`` context manager: with client.transport.tune(http_timeout=120.0): my_groups = client.get_my_groups() -For users who are customizing the transport class for a custom client, update like so: +Retry Check Mechanisms Moved to ``request_retry_checks`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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, ``request_retry_checks``. These can be directly +inspected and modified, and separate per-service configuration of these checks +from the user-instantiable ``transport`` object. + +These changes impact users who were using a custom ``RequestsTransport`` class. +The transport class no longer defines the HTTP status codes which drive the +default retry checks. +These capabilities have been moved to +``globus_sdk.transport.DefaultRetryCheckCollection``, a new object which is +configured on clients and which can be reconfigured in order to change these +check behaviors. + +For example, users could previously declare a custom transport type which +treats only 502s as transient errors which may resolve with a simple retry. +This could then be configured on a custom client class: .. code-block:: python # globus-sdk v3 import globus_sdk - from .mylibrary import CustomTransport + from globus_sdk.transport import RequestsTransport + + + class MyTransport(RequestsTransport): + TRANSIENT_ERROR_STATUS_CODES = (502,) - class MyClient(globus_sdk.GroupsClient): - transport_class = CustomTransport + class MyClientClass(globus_sdk.GroupsClient): + transport_class = MyTransport + client = MyClientClass() + +Because the ``transport_class`` has been removed from clients, this mechanism +has changed. +In order to customize the same information, users should first instantiate a +client and then modify the attributes of the ``request_retry_checks`` object: + +.. code-block:: python + # globus-sdk v4 import globus_sdk - from .mylibrary import CustomTransport + client = globus_sdk.GroupsClient() + client.request_retry_checks.transient_error_status_codes = (502,) + +.. note:: - class MyClient(globus_sdk.GroupsClient): - default_transport_factory = CustomTransport + Client classes may use types for ``request_retry_checks`` other than + ``DefaultRetryCheckCollection``, but all SDK-defined clients use subclasses + of this type. From 1.x or 2.x to 3.0 ----------------------- diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 53dcb5b23..a5ffe68a4 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -12,7 +12,12 @@ 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 ( + DefaultRetryCheckCollection, + RequestCallerInfo, + RequestsTransport, + RetryCheckCollection, +) if sys.version_info >= (3, 10): from typing import TypeAlias @@ -51,7 +56,9 @@ class BaseClient: :param transport: A :class:`RequestsTransport` object for sending and retrying requests. By default, one will be constructed by the client. - All other parameters are for internal use and should be ignored. + :ivar RetryCheckCollection request_retry_checks: The retry checks for a + given client, as an ordered collection. These determine which requests will + be retried on failure. """ # service name is used to lookup a service URL from config @@ -66,10 +73,6 @@ class BaseClient: #: this can be set in subclasses, but must always be a subclass of GlobusError error_class: type[exc.GlobusAPIError] = exc.GlobusAPIError - #: a function or class which will be used to construct a transport if one - #: is not provided, defaults to ``RequestsTransport`` - default_transport_factory: t.Callable[[], RequestsTransport] = RequestsTransport - #: the scopes for this client may be present as a ``ScopeCollection`` scopes: ScopeCollection | None = None @@ -109,9 +112,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 = ( - transport if transport is not None else self.default_transport_factory() + self.request_retry_checks: RetryCheckCollection = ( + self._get_default_retry_checks() ) + self.transport = transport if transport is not None else RequestsTransport() log.debug(f"initialized transport of type {type(self.transport)}") # setup paginated methods @@ -143,6 +147,14 @@ def default_scope_requirements(self) -> list[Scope]: """ raise NotImplementedError + def _get_default_retry_checks(self) -> RetryCheckCollection: + """ + Create the default for 'request_retry_checks'. + + This is called during init and may be overridden by subclasses. + """ + return DefaultRetryCheckCollection() + @classmethod def _resolve_base_url(cls, init_base_url: str | None, environment: str) -> str: """ @@ -500,8 +512,11 @@ 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_checks=self.request_retry_checks, + authorizer=authorizer, + ) # make the request log.debug("request will hit URL: %s", url) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index c877173b5..d4ff48c4b 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -15,7 +15,7 @@ from .data import DeleteData, TransferData from .errors import TransferAPIError from .response import IterableTransferResponse -from .transport import TransferRequestsTransport +from .transport import TransferDefaultRetryCheckCollection log = logging.getLogger(__name__) @@ -129,13 +129,14 @@ class TransferClient(client.BaseClient): """ service_name = "transfer" - default_transport_factory: t.Callable[[], TransferRequestsTransport] = ( - TransferRequestsTransport - ) error_class = TransferAPIError scopes = TransferScopes default_scope_requirements = [TransferScopes.all] + def _get_default_retry_checks(self) -> TransferDefaultRetryCheckCollection: + """Override the default retry checks.""" + return TransferDefaultRetryCheckCollection() + def add_app_data_access_scope( self, collection_ids: uuid.UUID | str | t.Iterable[uuid.UUID | str] ) -> TransferClient: diff --git a/src/globus_sdk/services/transfer/transport.py b/src/globus_sdk/services/transfer/transport.py index d4688ec10..693e673ed 100644 --- a/src/globus_sdk/services/transfer/transport.py +++ b/src/globus_sdk/services/transfer/transport.py @@ -1,13 +1,17 @@ """ -Custom Transport class for the TransferClient that overrides -default_check_transient_error +Custom retry check collection for the TransferClient that overrides +the default check_transient_error """ -from globus_sdk.transport import RequestsTransport, RetryCheckResult, RetryContext +from globus_sdk.transport import ( + DefaultRetryCheckCollection, + RetryCheckResult, + RetryContext, +) -class TransferRequestsTransport(RequestsTransport): - def default_check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: +class TransferDefaultRetryCheckCollection(DefaultRetryCheckCollection): + def check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: """ check for transient error status codes which could be resolved by retrying the request. Does not retry ExternalErrors or EndpointErrors @@ -17,7 +21,7 @@ def default_check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: retries which may already have been attempted """ if ctx.response is not None and ( - ctx.response.status_code in self.TRANSIENT_ERROR_STATUS_CODES + ctx.response.status_code in self.transient_error_status_codes ): try: code = ctx.response.json()["code"] diff --git a/src/globus_sdk/transport/__init__.py b/src/globus_sdk/transport/__init__.py index 3fdc2fb47..75f9d6530 100644 --- a/src/globus_sdk/transport/__init__.py +++ b/src/globus_sdk/transport/__init__.py @@ -1,8 +1,11 @@ from ._clientinfo import GlobusClientInfo +from .caller_info import RequestCallerInfo +from .default_retry_checks import DefaultRetryCheckCollection from .encoders import FormRequestEncoder, JSONRequestEncoder, RequestEncoder -from .requests import RequestCallerInfo, RequestsTransport +from .requests import RequestsTransport from .retry import ( RetryCheck, + RetryCheckCollection, RetryCheckFlags, RetryCheckResult, RetryCheckRunner, @@ -14,11 +17,13 @@ "RequestsTransport", "RequestCallerInfo", "RetryCheck", + "RetryCheckCollection", "RetryCheckFlags", "RetryCheckResult", "RetryCheckRunner", "set_retry_check_flags", "RetryContext", + "DefaultRetryCheckCollection", "RequestEncoder", "JSONRequestEncoder", "FormRequestEncoder", diff --git a/src/globus_sdk/transport/caller_info.py b/src/globus_sdk/transport/caller_info.py new file mode 100644 index 000000000..f22c060b7 --- /dev/null +++ b/src/globus_sdk/transport/caller_info.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from globus_sdk.authorizers import GlobusAuthorizer + +from .retry import RetryCheckCollection + + +class RequestCallerInfo: + """ + Data object that holds contextual information about the caller of a request. + + :param retry_checks: The configured retry checks for the call + :param authorizer: The authorizer object from the client making the request + """ + + def __init__( + self, + *, + retry_checks: RetryCheckCollection, + authorizer: GlobusAuthorizer | None = None, + ) -> None: + self.authorizer = authorizer + self.retry_checks = retry_checks diff --git a/src/globus_sdk/transport/default_retry_checks.py b/src/globus_sdk/transport/default_retry_checks.py new file mode 100644 index 000000000..066d2b495 --- /dev/null +++ b/src/globus_sdk/transport/default_retry_checks.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import requests + +from .retry import ( + RetryCheckCollection, + RetryCheckFlags, + RetryCheckResult, + RetryContext, + set_retry_check_flags, +) + + +class DefaultRetryCheckCollection(RetryCheckCollection): + """The default checks for the SDK. + + :param retry_after_status_codes: status codes for responses which may have + a Retry-After header + :param transient_error_status_codes: status codes for error responses which + should generally be retried + :param expired_authorization_status_codes: status codes indicating that + authorization info was missing or expired + """ + + def __init__( + self, + *, + retry_after_status_codes: tuple[int, ...] = (429, 503), + transient_error_status_codes: tuple[int, ...] = (429, 500, 502, 503, 504), + expired_authorization_status_codes: tuple[int, ...] = (401,), + ) -> None: + super().__init__() + + self.retry_after_status_codes = retry_after_status_codes + self.transient_error_status_codes = transient_error_status_codes + self.expired_authorization_status_codes = expired_authorization_status_codes + + self.register_check(self.check_expired_authorization) + self.register_check(self.check_request_exception) + self.register_check(self.check_retry_after_header) + self.register_check(self.check_transient_error) + + def check_request_exception(self, ctx: RetryContext) -> RetryCheckResult: + """ + Check if a network error was encountered + + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + if ctx.exception and isinstance(ctx.exception, requests.RequestException): + return RetryCheckResult.do_retry + return RetryCheckResult.no_decision + + def check_retry_after_header(self, ctx: RetryContext) -> RetryCheckResult: + """ + Check for a retry-after header if the response had a matching status + + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + if ( + ctx.response is None + or ctx.response.status_code not in self.retry_after_status_codes + ): + return RetryCheckResult.no_decision + retry_after = self.parse_retry_after(ctx.response) + if retry_after: + ctx.backoff = float(retry_after) + return RetryCheckResult.do_retry + + def check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: + """ + Check for transient error status codes which could be resolved by retrying + the request + + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + if ctx.response is not None and ( + ctx.response.status_code in self.transient_error_status_codes + ): + return RetryCheckResult.do_retry + return RetryCheckResult.no_decision + + @set_retry_check_flags(RetryCheckFlags.RUN_ONCE) + def check_expired_authorization(self, ctx: RetryContext) -> RetryCheckResult: + """ + This check evaluates whether or not there is invalid or expired authorization + information which could be updated with some action -- most typically a token + refresh for an expired access token. + + The check is flagged to only run once per request. + + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + if ( # is the current check applicable? + ctx.response is None + or ctx.caller_info is None + or ctx.caller_info.authorizer is None + or ctx.response.status_code not in self.expired_authorization_status_codes + ): + return RetryCheckResult.no_decision + + # run the authorizer's handler, and 'do_retry' if the handler indicated + # that it was able to make a change which should make the request retryable + if ctx.caller_info.authorizer.handle_missing_authorization(): + return RetryCheckResult.do_retry + return RetryCheckResult.no_decision + + def parse_retry_after(self, response: requests.Response) -> int | None: + """Get the 'Retry-After' header as an int.""" + val = response.headers.get("Retry-After") + if not val: + return None + try: + return int(val) + except ValueError: + return None diff --git a/src/globus_sdk/transport/requests.py b/src/globus_sdk/transport/requests.py index 649f060fb..b846c0d2e 100644 --- a/src/globus_sdk/transport/requests.py +++ b/src/globus_sdk/transport/requests.py @@ -18,39 +18,15 @@ ) from ._clientinfo import GlobusClientInfo +from .caller_info import RequestCallerInfo from .retry import ( - RetryCheck, - RetryCheckFlags, - RetryCheckResult, RetryCheckRunner, RetryContext, - set_retry_check_flags, ) log = logging.getLogger(__name__) -class RequestCallerInfo: - """ - Data object that holds contextual information about the caller of a request. - - :param authorizer: The authorizer object from the client making the request - """ - - def __init__(self, *, authorizer: GlobusAuthorizer | None = None) -> None: - self.authorizer = authorizer - - -def _parse_retry_after(response: requests.Response) -> int | None: - val = response.headers.get("Retry-After") - if not val: - return None - try: - return int(val) - except ValueError: - return None - - def _exponential_backoff(ctx: RetryContext) -> float: # respect any explicit backoff set on the context if ctx.backoff is not None: @@ -85,8 +61,6 @@ class RequestsTransport: :param retry_backoff: A function which determines how long to sleep between calls based on the RetryContext. Defaults to exponential backoff with jitter based on the context ``attempt`` number. - :param retry_checks: A list of initial retry checks. Any hooks registered, - including the default hooks, will run after these checks. :param max_sleep: The maximum sleep time between retries (in seconds). If the computed sleep time or the backoff requested by a retry check exceeds this value, this amount of time will be used instead @@ -99,13 +73,6 @@ class RequestsTransport: #: default maximum number of retries DEFAULT_MAX_RETRIES = 5 - #: status codes for responses which may have a Retry-After header - RETRY_AFTER_STATUS_CODES: tuple[int, ...] = (429, 503) - #: status codes for error responses which should generally be retried - TRANSIENT_ERROR_STATUS_CODES: tuple[int, ...] = (429, 500, 502, 503, 504) - #: status codes indicating that authorization info was missing or expired - EXPIRED_AUTHORIZATION_STATUS_CODES: tuple[int, ...] = (401,) - #: the encoders are a mapping of encoding names to encoder objects encoders: dict[str, RequestEncoder] = { "text": RequestEncoder(), @@ -120,7 +87,6 @@ def __init__( verify_ssl: bool | str | pathlib.Path | None = None, http_timeout: float | None = None, retry_backoff: t.Callable[[RetryContext], float] = _exponential_backoff, - retry_checks: list[RetryCheck] | None = None, max_sleep: float | int = 10, max_retries: int | None = None, ) -> None: @@ -143,9 +109,6 @@ def __init__( self.max_retries = ( max_retries if max_retries is not None else self.DEFAULT_MAX_RETRIES ) - self.retry_checks = list(retry_checks if retry_checks else []) # copy - # register internal checks - self.register_default_retry_checks() def close(self) -> None: """ @@ -346,7 +309,7 @@ def request( log.debug("starting request for %s", url) resp: requests.Response | None = None req = self._encode(method, url, query_params, data, headers, encoding) - checker = RetryCheckRunner(self.retry_checks) + checker = RetryCheckRunner(caller_info.retry_checks.checks) log.debug("transport request state initialized") for attempt in range(self.max_retries + 1): @@ -388,107 +351,3 @@ def request( raise ValueError("Somehow, retries ended without a response") log.warning("request reached max retries, done (fail, response)") return resp - - # decorator which lets you add a check to a retry policy - def register_retry_check(self, func: RetryCheck) -> RetryCheck: - """ - Register a retry check with this transport. - - A retry checker is a callable responsible for implementing - `check(RetryContext) -> RetryCheckResult` - - `check` should *not* perform any sleeps or delays. - Multiple checks should be chainable and callable in any order. - - :param func: The function or other callable to register as a retry check - """ - self.retry_checks.append(func) - return func - - def register_default_retry_checks(self) -> None: - """ - This hook is called during transport initialization. By default, it registers - the following hooks: - - - default_check_expired_authorization - - default_check_request_exception - - default_check_retry_after_header - - default_check_transient_error - - It can be overridden to register additional hooks or to remove the default - hooks. - """ - self.register_retry_check(self.default_check_expired_authorization) - self.register_retry_check(self.default_check_request_exception) - self.register_retry_check(self.default_check_retry_after_header) - self.register_retry_check(self.default_check_transient_error) - - def default_check_request_exception(self, ctx: RetryContext) -> RetryCheckResult: - """ - Check if a network error was encountered - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - if ctx.exception and isinstance(ctx.exception, requests.RequestException): - return RetryCheckResult.do_retry - return RetryCheckResult.no_decision - - def default_check_retry_after_header(self, ctx: RetryContext) -> RetryCheckResult: - """ - Check for a retry-after header if the response had a matching status - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - if ( - ctx.response is None - or ctx.response.status_code not in self.RETRY_AFTER_STATUS_CODES - ): - return RetryCheckResult.no_decision - retry_after = _parse_retry_after(ctx.response) - if retry_after: - ctx.backoff = float(retry_after) - return RetryCheckResult.do_retry - - def default_check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: - """ - Check for transient error status codes which could be resolved by retrying - the request - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - if ctx.response is not None and ( - ctx.response.status_code in self.TRANSIENT_ERROR_STATUS_CODES - ): - return RetryCheckResult.do_retry - return RetryCheckResult.no_decision - - @set_retry_check_flags(RetryCheckFlags.RUN_ONCE) - def default_check_expired_authorization( - self, ctx: RetryContext - ) -> RetryCheckResult: - """ - This check evaluates whether or not there is invalid or expired authorization - information which could be updated with some action -- most typically a token - refresh for an expired access token. - - The check is flagged to only run once per request. - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - if ( # is the current check applicable? - ctx.response is None - or ctx.caller_info is None - or ctx.caller_info.authorizer is None - or ctx.response.status_code not in self.EXPIRED_AUTHORIZATION_STATUS_CODES - ): - return RetryCheckResult.no_decision - - # run the authorizer's handler, and 'do_retry' if the handler indicated - # that it was able to make a change which should make the request retryable - if ctx.caller_info.authorizer.handle_missing_authorization(): - return RetryCheckResult.do_retry - return RetryCheckResult.no_decision diff --git a/src/globus_sdk/transport/retry.py b/src/globus_sdk/transport/retry.py index 54202017a..439abe79f 100644 --- a/src/globus_sdk/transport/retry.py +++ b/src/globus_sdk/transport/retry.py @@ -7,7 +7,7 @@ import requests if t.TYPE_CHECKING: - from globus_sdk.transport.requests import RequestCallerInfo + from .caller_info import RequestCallerInfo log = logging.getLogger(__name__) @@ -117,7 +117,7 @@ class RetryCheckRunner: # check configs: a list of pairs, (check, flags) # a check without flags is assumed to have flags=NONE - def __init__(self, checks: list[RetryCheck]) -> None: + def __init__(self, checks: t.Iterable[RetryCheck]) -> None: self._checks: list[RetryCheck] = [] self._check_data: dict[RetryCheck, dict[str, t.Any]] = {} for check in checks: @@ -147,3 +147,40 @@ def should_retry(self, context: RetryContext) -> bool: # fallthrough: don't retry any request which isn't marked for retry return False + + +class RetryCheckCollection: + """ + A RetryCheckCollection is an ordered collection of retry checks which are + used to determine whether or not a request should be retried. + + Notably, the collection does not decide + - how many times a request should retry + - how or how long the call should wait between attempts + (except via the backoff which may be set) + - what kinds of request parameters (e.g., timeouts) are used + + It *only* contains `RetryCheck` methods which can look at a response or + error and decide whether or not to retry. + """ + + def __init__(self) -> None: + self.checks: list[RetryCheck] = [] + + def register_check(self, func: RetryCheck) -> RetryCheck: + """ + Register a retry check with this policy. + + A retry checker is a callable responsible for implementing + `check(RetryContext) -> RetryCheckResult` + + `check` should *not* perform any sleeps or delays. + Multiple checks should be chainable and callable in any order. + + :param func: The function or other callable to register as a retry check + """ + self.checks.append(func) + return func + + def __iter__(self) -> t.Iterator[RetryCheck]: + yield from self.checks diff --git a/tests/conftest.py b/tests/conftest.py index 44b5fd6cc..3b346007e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,10 +15,7 @@ def mocksleep(): @pytest.fixture def no_retry_transport(): - class NoRetryTransport(RequestsTransport): - DEFAULT_MAX_RETRIES = 0 - - return NoRetryTransport + return RequestsTransport(max_retries=0) @pytest.fixture(autouse=True) diff --git a/tests/functional/base_client/test_retry_behavior.py b/tests/functional/base_client/test_retry_behavior.py index fa5a83ea4..570746a04 100644 --- a/tests/functional/base_client/test_retry_behavior.py +++ b/tests/functional/base_client/test_retry_behavior.py @@ -293,7 +293,9 @@ def handle_missing_authorization(self): return True authorizer = DummyAuthorizer() - caller_info = RequestCallerInfo(authorizer=authorizer) + caller_info = RequestCallerInfo( + retry_checks=client.request_retry_checks, authorizer=authorizer + ) # Test direct transport usage with caller_info response = client.transport.request( diff --git a/tests/functional/services/auth/confidential_client/conftest.py b/tests/functional/services/auth/confidential_client/conftest.py index 0653e7b83..0486e7dea 100644 --- a/tests/functional/services/auth/confidential_client/conftest.py +++ b/tests/functional/services/auth/confidential_client/conftest.py @@ -5,7 +5,6 @@ @pytest.fixture def auth_client(no_retry_transport): - class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): - default_transport_factory = no_retry_transport - - return CustomAuthClient("dummy_client_id", "dummy_client_secret") + return globus_sdk.ConfidentialAppAuthClient( + "dummy_client_id", "dummy_client_secret", transport=no_retry_transport + ) diff --git a/tests/functional/services/auth/conftest.py b/tests/functional/services/auth/conftest.py index c6071ad37..549584ddf 100644 --- a/tests/functional/services/auth/conftest.py +++ b/tests/functional/services/auth/conftest.py @@ -5,15 +5,9 @@ @pytest.fixture def login_client(no_retry_transport): - class CustomAuthClient(globus_sdk.AuthLoginClient): - default_transport_factory = no_retry_transport - - return CustomAuthClient() + return globus_sdk.AuthLoginClient(transport=no_retry_transport) @pytest.fixture def service_client(no_retry_transport): - class CustomAuthClient(globus_sdk.AuthClient): - default_transport_factory = no_retry_transport - - return CustomAuthClient() + return globus_sdk.AuthClient(transport=no_retry_transport) diff --git a/tests/functional/services/auth/native_client/conftest.py b/tests/functional/services/auth/native_client/conftest.py index 0d9168d6f..b67ad4633 100644 --- a/tests/functional/services/auth/native_client/conftest.py +++ b/tests/functional/services/auth/native_client/conftest.py @@ -5,7 +5,6 @@ @pytest.fixture def auth_client(no_retry_transport): - class CustomAuthClient(globus_sdk.NativeAppAuthClient): - default_transport_factory = no_retry_transport - - return CustomAuthClient("dummy_client_id") + return globus_sdk.NativeAppAuthClient( + "dummy_client_id", transport=no_retry_transport + ) diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index 7bc96aedd..b819eecb2 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -14,19 +14,17 @@ @pytest.fixture def native_client(no_retry_transport): - class CustomAuthClient(globus_sdk.NativeAppAuthClient): - default_transport_factory = no_retry_transport - - return CustomAuthClient(client_id=CLIENT_ID) + return globus_sdk.NativeAppAuthClient( + client_id=CLIENT_ID, transport=no_retry_transport + ) @pytest.fixture def confidential_client(no_retry_transport): - class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): - default_transport_factory = no_retry_transport - - return CustomAuthClient( - client_id=CLIENT_ID, client_secret="SECRET_SECRET_HES_GOT_A_SECRET" + return globus_sdk.ConfidentialAppAuthClient( + client_id=CLIENT_ID, + client_secret="SECRET_SECRET_HES_GOT_A_SECRET", + transport=no_retry_transport, ) diff --git a/tests/functional/services/compute/conftest.py b/tests/functional/services/compute/conftest.py index 4755a7f71..604b9cbd2 100644 --- a/tests/functional/services/compute/conftest.py +++ b/tests/functional/services/compute/conftest.py @@ -5,15 +5,9 @@ @pytest.fixture def compute_client_v2(no_retry_transport): - class CustomComputeClientV2(globus_sdk.ComputeClientV2): - default_transport_factory = no_retry_transport - - return CustomComputeClientV2() + return globus_sdk.ComputeClientV2(transport=no_retry_transport) @pytest.fixture def compute_client_v3(no_retry_transport): - class CustomComputeClientV3(globus_sdk.ComputeClientV3): - default_transport_factory = no_retry_transport - - return CustomComputeClientV3() + return globus_sdk.ComputeClientV3(transport=no_retry_transport) diff --git a/tests/functional/services/flows/conftest.py b/tests/functional/services/flows/conftest.py index ea982f41e..0d7224e0b 100644 --- a/tests/functional/services/flows/conftest.py +++ b/tests/functional/services/flows/conftest.py @@ -7,10 +7,7 @@ @pytest.fixture def flows_client(no_retry_transport): - class CustomFlowsClient(globus_sdk.FlowsClient): - default_transport_factory = no_retry_transport - - return CustomFlowsClient() + return globus_sdk.FlowsClient(transport=no_retry_transport) @pytest.fixture @@ -18,6 +15,8 @@ def specific_flow_client_class( no_retry_transport, ) -> t.Type[globus_sdk.SpecificFlowClient]: class CustomSpecificFlowClient(globus_sdk.SpecificFlowClient): - default_transport_factory = no_retry_transport + def __init__(self, **kwargs) -> None: + kwargs["transport"] = no_retry_transport + super().__init__(**kwargs) return CustomSpecificFlowClient diff --git a/tests/functional/services/gcs/conftest.py b/tests/functional/services/gcs/conftest.py index b4743c4fe..5a17a9515 100644 --- a/tests/functional/services/gcs/conftest.py +++ b/tests/functional/services/gcs/conftest.py @@ -1,12 +1,9 @@ import pytest -from globus_sdk import GCSClient +import globus_sdk @pytest.fixture def client(no_retry_transport): - class CustomGCSClient(GCSClient): - default_transport_factory = no_retry_transport - # default fqdn for GCS client testing - return CustomGCSClient("abc.xyz.data.globus.org") + return globus_sdk.GCSClient("abc.xyz.data.globus.org", transport=no_retry_transport) diff --git a/tests/functional/services/groups/conftest.py b/tests/functional/services/groups/conftest.py index 9c2c1c7ff..eb5018ac4 100644 --- a/tests/functional/services/groups/conftest.py +++ b/tests/functional/services/groups/conftest.py @@ -5,10 +5,7 @@ @pytest.fixture def groups_client(no_retry_transport): - class CustomGroupsClient(globus_sdk.GroupsClient): - default_transport_factory = no_retry_transport - - return CustomGroupsClient() + return globus_sdk.GroupsClient(transport=no_retry_transport) @pytest.fixture diff --git a/tests/functional/services/search/conftest.py b/tests/functional/services/search/conftest.py index ae3e84649..24f496263 100644 --- a/tests/functional/services/search/conftest.py +++ b/tests/functional/services/search/conftest.py @@ -5,7 +5,4 @@ @pytest.fixture def client(no_retry_transport): - class CustomSearchClient(globus_sdk.SearchClient): - default_transport_factory = no_retry_transport - - return CustomSearchClient() + return globus_sdk.SearchClient(transport=no_retry_transport) diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index e09e63d68..a08269d16 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -13,10 +13,7 @@ @pytest.fixture def search_client(no_retry_transport): - class CustomSearchClient(globus_sdk.SearchClient): - default_transport_factory = no_retry_transport - - return CustomSearchClient() + return globus_sdk.SearchClient(transport=no_retry_transport) def test_search_query_simple(search_client): diff --git a/tests/functional/services/search/test_search_roles.py b/tests/functional/services/search/test_search_roles.py index 51e6ded10..8c2f13f72 100644 --- a/tests/functional/services/search/test_search_roles.py +++ b/tests/functional/services/search/test_search_roles.py @@ -8,10 +8,7 @@ @pytest.fixture def search_client(no_retry_transport): - class CustomSearchClient(globus_sdk.SearchClient): - default_transport_factory = no_retry_transport - - return CustomSearchClient() + return globus_sdk.SearchClient(transport=no_retry_transport) def test_search_role_create(search_client): diff --git a/tests/functional/services/transfer/conftest.py b/tests/functional/services/transfer/conftest.py index b309c233a..9bead524c 100644 --- a/tests/functional/services/transfer/conftest.py +++ b/tests/functional/services/transfer/conftest.py @@ -5,7 +5,4 @@ @pytest.fixture def client(no_retry_transport): - class CustomTransferClient(globus_sdk.TransferClient): - default_transport_factory = no_retry_transport - - return CustomTransferClient() + return globus_sdk.TransferClient(transport=no_retry_transport) diff --git a/tests/functional/tokenstorage/v2/conftest.py b/tests/functional/tokenstorage/v2/conftest.py index 7c46341c6..52b42023c 100644 --- a/tests/functional/tokenstorage/v2/conftest.py +++ b/tests/functional/tokenstorage/v2/conftest.py @@ -16,10 +16,9 @@ def id_token_sub(): @pytest.fixture def cc_auth_client(no_retry_transport): - class CustomAuthClient(globus_sdk.ConfidentialAppAuthClient): - default_transport_factory = no_retry_transport - - return CustomAuthClient("dummy_id", "dummy_secret") + return globus_sdk.ConfidentialAppAuthClient( + "dummy_id", "dummy_secret", transport=no_retry_transport + ) @pytest.fixture diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index 81394cb53..691971c37 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -23,10 +23,13 @@ def auth_client(): def base_client_class(no_retry_transport): class CustomClient(globus_sdk.BaseClient): service_name = "transfer" - default_transport_factory = no_retry_transport scopes = TransferScopes default_scope_requirements = [TransferScopes.all] + def __init__(self, **kwargs) -> None: + kwargs["transport"] = no_retry_transport + super().__init__(**kwargs) + return CustomClient diff --git a/tests/unit/transport/test_default_retry_policy.py b/tests/unit/transport/test_default_retry_policy.py index 9c13e296b..3d4cc715d 100644 --- a/tests/unit/transport/test_default_retry_policy.py +++ b/tests/unit/transport/test_default_retry_policy.py @@ -3,6 +3,7 @@ import pytest from globus_sdk.transport import ( + DefaultRetryCheckCollection, RequestCallerInfo, RequestsTransport, RetryCheckResult, @@ -13,13 +14,14 @@ @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_respects_retry_after(mocksleep, http_status): + retry_checks = DefaultRetryCheckCollection() transport = RequestsTransport() - checker = RetryCheckRunner(transport.retry_checks) + checker = RetryCheckRunner(retry_checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "5"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=retry_checks) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True @@ -32,12 +34,13 @@ def test_retry_policy_respects_retry_after(mocksleep, http_status): def test_retry_policy_ignores_retry_after_too_high(mocksleep, http_status): # set explicit max sleep to confirm that the value is capped here transport = RequestsTransport(max_sleep=5) - checker = RetryCheckRunner(transport.retry_checks) + retry_checks = DefaultRetryCheckCollection() + checker = RetryCheckRunner(retry_checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "20"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=retry_checks) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True @@ -49,12 +52,13 @@ def test_retry_policy_ignores_retry_after_too_high(mocksleep, http_status): @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): transport = RequestsTransport() - checker = RetryCheckRunner(transport.retry_checks) + retry_checks = DefaultRetryCheckCollection() + checker = RetryCheckRunner(retry_checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "not-an-integer"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=retry_checks) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True @@ -66,29 +70,13 @@ def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): @pytest.mark.parametrize( "checkname", [ - "default_check_retry_after_header", - "default_check_transient_error", + "check_retry_after_header", + "check_transient_error", ], ) def test_default_retry_check_noop_on_exception(checkname, mocksleep): - transport = RequestsTransport() - method = getattr(transport, checkname) - caller_info = RequestCallerInfo(authorizer=None) + retry_checks = DefaultRetryCheckCollection() + method = getattr(retry_checks, checkname) + caller_info = RequestCallerInfo(retry_checks=retry_checks) ctx = RetryContext(1, caller_info=caller_info, exception=Exception("foo")) assert method(ctx) is RetryCheckResult.no_decision - - -def test_retry_context_accepts_caller_info(): - mock_authorizer = mock.Mock() - caller_info = RequestCallerInfo(authorizer=mock_authorizer) - - ctx = RetryContext(1, caller_info=caller_info) - - assert ctx.caller_info is caller_info - assert ctx.caller_info.authorizer is mock_authorizer - - -def test_retry_context_caller_info_none(): - ctx = RetryContext(1, caller_info=None) - - assert ctx.caller_info is None diff --git a/tests/unit/transport/test_retry_check_runner.py b/tests/unit/transport/test_retry_check_runner.py index 9d095ccf0..6932963cd 100644 --- a/tests/unit/transport/test_retry_check_runner.py +++ b/tests/unit/transport/test_retry_check_runner.py @@ -1,6 +1,7 @@ from unittest import mock from globus_sdk.transport import ( + DefaultRetryCheckCollection, RequestCallerInfo, RetryCheckResult, RetryCheckRunner, @@ -9,7 +10,7 @@ def _make_test_retry_context(*, status=200, exception=None, response=None): - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=DefaultRetryCheckCollection()) if exception: return RetryContext(1, caller_info=caller_info, exception=exception) elif response: diff --git a/tests/unit/transport/test_transfer_transport.py b/tests/unit/transport/test_transfer_transport.py index 7fb93c1ff..0f98e063f 100644 --- a/tests/unit/transport/test_transfer_transport.py +++ b/tests/unit/transport/test_transfer_transport.py @@ -1,12 +1,12 @@ from unittest import mock -from globus_sdk.services.transfer.transport import TransferRequestsTransport +from globus_sdk.services.transfer.transport import TransferDefaultRetryCheckCollection from globus_sdk.transport import RequestCallerInfo, RetryCheckRunner, RetryContext def test_transfer_does_not_retry_external(): - transport = TransferRequestsTransport() - checker = RetryCheckRunner(transport.retry_checks) + retry_checks = TransferDefaultRetryCheckCollection() + checker = RetryCheckRunner(retry_checks) body = { "HTTP status": "502", @@ -19,15 +19,15 @@ def test_transfer_does_not_retry_external(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=retry_checks) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False def test_transfer_does_not_retry_endpoint_error(): - transport = TransferRequestsTransport() - checker = RetryCheckRunner(transport.retry_checks) + retry_checks = TransferDefaultRetryCheckCollection() + checker = RetryCheckRunner(retry_checks) body = { "HTTP status": "502", @@ -43,15 +43,15 @@ def test_transfer_does_not_retry_endpoint_error(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=retry_checks) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False def test_transfer_retries_others(): - transport = TransferRequestsTransport() - checker = RetryCheckRunner(transport.retry_checks) + retry_checks = TransferDefaultRetryCheckCollection() + checker = RetryCheckRunner(retry_checks) def _raise_value_error(): raise ValueError() @@ -59,7 +59,7 @@ def _raise_value_error(): dummy_response = mock.Mock() dummy_response.json = _raise_value_error dummy_response.status_code = 502 - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=retry_checks) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True diff --git a/tests/unit/transport/test_transport_authz_handling.py b/tests/unit/transport/test_transport_authz_handling.py index bfa23f599..9847fbf49 100644 --- a/tests/unit/transport/test_transport_authz_handling.py +++ b/tests/unit/transport/test_transport_authz_handling.py @@ -3,7 +3,11 @@ import pytest from globus_sdk.authorizers import NullAuthorizer -from globus_sdk.transport import RequestCallerInfo, RequestsTransport +from globus_sdk.transport import ( + DefaultRetryCheckCollection, + RequestCallerInfo, + RequestsTransport, +) def test_will_not_modify_authz_header_without_authorizer(): @@ -32,18 +36,13 @@ def test_will_null_authz_header_with_null_authorizer(): assert request.headers == {} -def test_request_caller_info_creation(): - mock_authorizer = mock.Mock() - caller_info = RequestCallerInfo(authorizer=mock_authorizer) - - assert caller_info.authorizer is mock_authorizer - - def test_requests_transport_accepts_caller_info(): transport = RequestsTransport() mock_authorizer = mock.Mock() mock_authorizer.get_authorization_header.return_value = "Bearer token" - caller_info = RequestCallerInfo(authorizer=mock_authorizer) + caller_info = RequestCallerInfo( + retry_checks=DefaultRetryCheckCollection(), authorizer=mock_authorizer + ) with mock.patch.object(transport, "session") as mock_session: mock_response = mock.Mock(status_code=200) @@ -68,12 +67,7 @@ def test_requests_transport_caller_info_required(): def test_requests_transport_keyword_only(): transport = RequestsTransport() - caller_info = RequestCallerInfo(authorizer=None) + caller_info = RequestCallerInfo(retry_checks=DefaultRetryCheckCollection()) with pytest.raises(TypeError): transport.request("GET", "https://example.com", caller_info) - - -def test_request_caller_info_with_none_authorizer(): - caller_info = RequestCallerInfo(authorizer=None) - assert caller_info.authorizer is None From 97390106f2a3650206aad51b7bb2bb13d4c46838 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 21 Jul 2025 19:55:46 -0500 Subject: [PATCH 154/176] Refactor to expose `RetryConfiguration` This change fully separates the retry config from the transport. RetryConfiguration is a "single pane of glass" view of how a client is configured to perform retries, clients now have a `retry_configuration` attribute which provides all of the retry-specific configuration. This combines details which were previously in the transport class and details which were attached to the retry check collection. Because the configuration is built per-client, it can be reconfigured by simple attribute access. For ease of use, the `tune()` context manager was added to the transport objects, and is therefore "ported" to the retry configuration. --- docs/upgrading.rst | 52 ++++----- src/globus_sdk/client.py | 21 ++-- src/globus_sdk/services/transfer/transport.py | 3 +- src/globus_sdk/transport/__init__.py | 4 +- src/globus_sdk/transport/caller_info.py | 8 +- .../transport/default_retry_checks.py | 45 ++++---- src/globus_sdk/transport/requests.py | 102 +++++------------- src/globus_sdk/transport/retry.py | 68 +----------- .../transport/retry_check_runner.py | 63 +++++++++++ src/globus_sdk/transport/retry_config.py | 97 +++++++++++++++++ tests/conftest.py | 6 -- .../base_client/test_retry_behavior.py | 10 +- .../auth/confidential_client/conftest.py | 8 +- tests/functional/services/auth/conftest.py | 12 ++- .../services/auth/native_client/conftest.py | 8 +- .../services/auth/test_auth_client_flow.py | 18 ++-- tests/functional/services/compute/conftest.py | 12 ++- tests/functional/services/flows/conftest.py | 12 +-- tests/functional/services/gcs/conftest.py | 6 +- tests/functional/services/groups/conftest.py | 6 +- tests/functional/services/search/conftest.py | 6 +- .../functional/services/search/test_search.py | 6 +- .../services/search/test_search_roles.py | 6 +- .../functional/services/transfer/conftest.py | 6 +- tests/functional/tokenstorage/v2/conftest.py | 8 +- tests/unit/test_base_client.py | 4 +- .../transport/test_default_retry_policy.py | 33 +++--- .../unit/transport/test_retry_check_runner.py | 4 +- .../unit/transport/test_transfer_transport.py | 25 +++-- tests/unit/transport/test_transport.py | 37 +++++-- .../test_transport_authz_handling.py | 7 +- 31 files changed, 401 insertions(+), 302 deletions(-) create mode 100644 src/globus_sdk/transport/retry_check_runner.py create mode 100644 src/globus_sdk/transport/retry_config.py diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7641300ad..03ce2c20f 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -487,28 +487,21 @@ or use the ``tune()`` context manager: with client.transport.tune(http_timeout=120.0): my_groups = client.get_my_groups() -Retry Check Mechanisms Moved to ``request_retry_checks`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retry Check Configuration Moved to ``retry_configuration`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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, ``request_retry_checks``. These can be directly -inspected and modified, and separate per-service configuration of these checks -from the user-instantiable ``transport`` object. - -These changes impact users who were using a custom ``RequestsTransport`` class. -The transport class no longer defines the HTTP status codes which drive the -default retry checks. -These capabilities have been moved to -``globus_sdk.transport.DefaultRetryCheckCollection``, a new object which is -configured on clients and which can be reconfigured in order to change these -check behaviors. - -For example, users could previously declare a custom transport type which -treats only 502s as transient errors which may resolve with a simple retry. +attribute of the client, ``retry_configuration``. + +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 @@ -528,10 +521,8 @@ This could then be configured on a custom client class: client = MyClientClass() -Because the ``transport_class`` has been removed from clients, this mechanism -has changed. -In order to customize the same information, users should first instantiate a -client and then modify the attributes of the ``request_retry_checks`` object: +Under SDK v4, in order to customize the same information, users can simply +client and then modify the attributes of the ``retry_configuration`` object: .. code-block:: python @@ -539,13 +530,24 @@ client and then modify the attributes of the ``request_retry_checks`` object: import globus_sdk client = globus_sdk.GroupsClient() - client.request_retry_checks.transient_error_status_codes = (502,) + client.retry_configuration.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_configuration.tune(max_retries=1): + my_groups = client.get_my_groups() -.. note:: - Client classes may use types for ``request_retry_checks`` other than - ``DefaultRetryCheckCollection``, but all SDK-defined clients use subclasses - of this type. From 1.x or 2.x to 3.0 ----------------------- diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index a5ffe68a4..5dcf26d29 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -16,7 +16,7 @@ DefaultRetryCheckCollection, RequestCallerInfo, RequestsTransport, - RetryCheckCollection, + RetryConfiguration, ) if sys.version_info >= (3, 10): @@ -56,9 +56,10 @@ class BaseClient: :param transport: A :class:`RequestsTransport` object for sending and retrying requests. By default, one will be constructed by the client. - :ivar RetryCheckCollection request_retry_checks: The retry checks for a - given client, as an ordered collection. These determine which requests will - be retried on failure. + :ivar RetryCheckCollection retry_configuration: The retry configuration for a + given client. This determines which requests will be retried on failure, + how many retries will be attempted, and how long the SDK may wait + between retries. """ # service name is used to lookup a service URL from config @@ -112,9 +113,7 @@ 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.request_retry_checks: RetryCheckCollection = ( - self._get_default_retry_checks() - ) + self.retry_configuration: RetryConfiguration = self._get_default_retry_config() self.transport = transport if transport is not None else RequestsTransport() log.debug(f"initialized transport of type {type(self.transport)}") @@ -147,13 +146,13 @@ def default_scope_requirements(self) -> list[Scope]: """ raise NotImplementedError - def _get_default_retry_checks(self) -> RetryCheckCollection: + def _get_default_retry_config(self) -> RetryConfiguration: """ - Create the default for 'request_retry_checks'. + Create the default retry configuration. This is called during init and may be overridden by subclasses. """ - return DefaultRetryCheckCollection() + return RetryConfiguration(checks=DefaultRetryCheckCollection()) @classmethod def _resolve_base_url(cls, init_base_url: str | None, environment: str) -> str: @@ -514,7 +513,7 @@ def request( # capture info about this client as the caller to pass to the transport caller_info = RequestCallerInfo( - retry_checks=self.request_retry_checks, + retry_configuration=self.retry_configuration, authorizer=authorizer, ) diff --git a/src/globus_sdk/services/transfer/transport.py b/src/globus_sdk/services/transfer/transport.py index 693e673ed..ef35d328e 100644 --- a/src/globus_sdk/services/transfer/transport.py +++ b/src/globus_sdk/services/transfer/transport.py @@ -20,8 +20,9 @@ def check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: :param ctx: The context object which describes the state of the request and the retries which may already have been attempted """ + retry_config = ctx.caller_info.retry_configuration if ctx.response is not None and ( - ctx.response.status_code in self.transient_error_status_codes + ctx.response.status_code in retry_config.transient_error_status_codes ): try: code = ctx.response.json()["code"] diff --git a/src/globus_sdk/transport/__init__.py b/src/globus_sdk/transport/__init__.py index 75f9d6530..4e1141d77 100644 --- a/src/globus_sdk/transport/__init__.py +++ b/src/globus_sdk/transport/__init__.py @@ -8,10 +8,11 @@ RetryCheckCollection, RetryCheckFlags, RetryCheckResult, - RetryCheckRunner, RetryContext, set_retry_check_flags, ) +from .retry_check_runner import RetryCheckRunner +from .retry_config import RetryConfiguration __all__ = ( "RequestsTransport", @@ -23,6 +24,7 @@ "RetryCheckRunner", "set_retry_check_flags", "RetryContext", + "RetryConfiguration", "DefaultRetryCheckCollection", "RequestEncoder", "JSONRequestEncoder", diff --git a/src/globus_sdk/transport/caller_info.py b/src/globus_sdk/transport/caller_info.py index f22c060b7..0295e3bee 100644 --- a/src/globus_sdk/transport/caller_info.py +++ b/src/globus_sdk/transport/caller_info.py @@ -2,22 +2,22 @@ from globus_sdk.authorizers import GlobusAuthorizer -from .retry import RetryCheckCollection +from .retry_config import RetryConfiguration class RequestCallerInfo: """ Data object that holds contextual information about the caller of a request. - :param retry_checks: The configured retry checks for the call + :param retry_config: The configuration of retry checks for the call :param authorizer: The authorizer object from the client making the request """ def __init__( self, *, - retry_checks: RetryCheckCollection, + retry_configuration: RetryConfiguration, authorizer: GlobusAuthorizer | None = None, ) -> None: self.authorizer = authorizer - self.retry_checks = retry_checks + self.retry_configuration = retry_configuration diff --git a/src/globus_sdk/transport/default_retry_checks.py b/src/globus_sdk/transport/default_retry_checks.py index 066d2b495..b448a94c3 100644 --- a/src/globus_sdk/transport/default_retry_checks.py +++ b/src/globus_sdk/transport/default_retry_checks.py @@ -12,28 +12,10 @@ class DefaultRetryCheckCollection(RetryCheckCollection): - """The default checks for the SDK. - - :param retry_after_status_codes: status codes for responses which may have - a Retry-After header - :param transient_error_status_codes: status codes for error responses which - should generally be retried - :param expired_authorization_status_codes: status codes indicating that - authorization info was missing or expired - """ - - def __init__( - self, - *, - retry_after_status_codes: tuple[int, ...] = (429, 503), - transient_error_status_codes: tuple[int, ...] = (429, 500, 502, 503, 504), - expired_authorization_status_codes: tuple[int, ...] = (401,), - ) -> None: - super().__init__() + """The default checks for the SDK.""" - self.retry_after_status_codes = retry_after_status_codes - self.transient_error_status_codes = transient_error_status_codes - self.expired_authorization_status_codes = expired_authorization_status_codes + def __init__(self) -> None: + super().__init__() self.register_check(self.check_expired_authorization) self.register_check(self.check_request_exception) @@ -58,9 +40,9 @@ def check_retry_after_header(self, ctx: RetryContext) -> RetryCheckResult: :param ctx: The context object which describes the state of the request and the retries which may already have been attempted. """ - if ( - ctx.response is None - or ctx.response.status_code not in self.retry_after_status_codes + retry_config = ctx.caller_info.retry_configuration + if ctx.response is None or ( + ctx.response.status_code not in retry_config.retry_after_status_codes ): return RetryCheckResult.no_decision retry_after = self.parse_retry_after(ctx.response) @@ -76,8 +58,9 @@ def check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: :param ctx: The context object which describes the state of the request and the retries which may already have been attempted. """ + retry_config = ctx.caller_info.retry_configuration if ctx.response is not None and ( - ctx.response.status_code in self.transient_error_status_codes + ctx.response.status_code in retry_config.transient_error_status_codes ): return RetryCheckResult.do_retry return RetryCheckResult.no_decision @@ -94,11 +77,15 @@ def check_expired_authorization(self, ctx: RetryContext) -> RetryCheckResult: :param ctx: The context object which describes the state of the request and the retries which may already have been attempted. """ + retry_config = ctx.caller_info.retry_configuration if ( # is the current check applicable? ctx.response is None or ctx.caller_info is None or ctx.caller_info.authorizer is None - or ctx.response.status_code not in self.expired_authorization_status_codes + or ( + ctx.response.status_code + not in retry_config.expired_authorization_status_codes + ) ): return RetryCheckResult.no_decision @@ -109,7 +96,11 @@ def check_expired_authorization(self, ctx: RetryContext) -> RetryCheckResult: return RetryCheckResult.no_decision def parse_retry_after(self, response: requests.Response) -> int | None: - """Get the 'Retry-After' header as an int.""" + """ + Get the 'Retry-After' header as an int. + + :param response: The response to parse. + """ val = response.headers.get("Retry-After") if not val: return None diff --git a/src/globus_sdk/transport/requests.py b/src/globus_sdk/transport/requests.py index b846c0d2e..8cf5dbb9f 100644 --- a/src/globus_sdk/transport/requests.py +++ b/src/globus_sdk/transport/requests.py @@ -3,7 +3,6 @@ import contextlib import logging import pathlib -import random import time import typing as t @@ -19,22 +18,13 @@ from ._clientinfo import GlobusClientInfo from .caller_info import RequestCallerInfo -from .retry import ( - RetryCheckRunner, - RetryContext, -) +from .retry import RetryContext +from .retry_check_runner import RetryCheckRunner +from .retry_config import RetryConfiguration log = logging.getLogger(__name__) -def _exponential_backoff(ctx: RetryContext) -> float: - # respect any explicit backoff set on the context - if ctx.backoff is not None: - return ctx.backoff - # exponential backoff with jitter - return t.cast(float, (0.25 + 0.5 * random.random()) * (2**ctx.attempt)) - - class RequestsTransport: """ The RequestsTransport handles HTTP request sending and retries. @@ -42,13 +32,9 @@ class RequestsTransport: It receives raw request information from a client class, and then performs the following steps - encode the data in a prepared request - - repeatedly send the request until no retry is requested + - repeatedly send the request until no retry is requested by the configured hooks - return the last response or reraise the last exception - Retry checks are registered as hooks on the Transport. Additional hooks can be - passed to the constructor via `retry_checks`. Or hooks can be added to an existing - transport via a decorator. - If the maximum number of retries is reached, the final response or exception will be returned or raised. @@ -58,13 +44,6 @@ class RequestsTransport: defaults to 60s but can be set via the ``GLOBUS_SDK_HTTP_TIMEOUT`` environment variable. Any value set via this parameter takes precedence over the environment variable. - :param retry_backoff: A function which determines how long to sleep between calls - based on the RetryContext. Defaults to exponential backoff with jitter based on - the context ``attempt`` number. - :param max_sleep: The maximum sleep time between retries (in seconds). If the - computed sleep time or the backoff requested by a retry check exceeds this - value, this amount of time will be used instead - :param max_retries: The maximum number of retries allowed by this transport :ivar dict[str, str] headers: The headers which are sent on every request. These may be augmented by the transport when sending requests. @@ -86,9 +65,6 @@ def __init__( self, verify_ssl: bool | str | pathlib.Path | None = None, http_timeout: float | None = None, - retry_backoff: t.Callable[[RetryContext], float] = _exponential_backoff, - max_sleep: float | int = 10, - max_retries: int | None = None, ) -> None: self.session = requests.Session() self.verify_ssl = config.get_ssl_verify(verify_ssl) @@ -103,13 +79,6 @@ def __init__( "X-Globus-Client-Info": self.globus_client_info.format(), } - # retry parameters - self.retry_backoff = retry_backoff - self.max_sleep = max_sleep - self.max_retries = ( - max_retries if max_retries is not None else self.DEFAULT_MAX_RETRIES - ) - def close(self) -> None: """ Closes all resources owned by the transport, primarily the underlying @@ -154,29 +123,17 @@ def tune( *, verify_ssl: bool | str | pathlib.Path | None = None, http_timeout: float | None = None, - retry_backoff: t.Callable[[RetryContext], float] | None = None, - max_sleep: float | int | None = None, - max_retries: int | None = None, ) -> t.Iterator[None]: """ Temporarily adjust some of the request sending settings of the transport. This method works as a context manager, and will reset settings to their original values after it exits. - In particular, this can be used to temporarily adjust request-sending minutiae - like the ``http_timeout`` used. - :param verify_ssl: Explicitly enable or disable SSL verification, or configure the path to a CA certificate bundle to use for SSL verification :param http_timeout: Explicitly set an HTTP timeout value in seconds - :param retry_backoff: A function which determines how long to sleep between - calls based on the RetryContext - :param max_sleep: The maximum sleep time between retries (in seconds). If the - computed sleep time or the backoff requested by a retry check exceeds this - value, this amount of time will be used instead - :param max_retries: The maximum number of retries allowed by this transport - **Examples** + **Example Usage** This can be used with any client class to temporarily set values in the context of one or more HTTP requests. To increase the HTTP request timeout from the @@ -186,19 +143,11 @@ def tune( >>> with client.transport.tune(http_timeout=120): >>> foo = client.get_foo() - or to disable retries (note that this also disables the retry on - expired-and-refreshed credentials): - - >>> client = ... # any client class - >>> with client.transport.tune(max_retries=0): - >>> foo = client.get_foo() + See also: :meth:`RetryConfiguration.tune`. """ saved_settings = ( self.verify_ssl, self.http_timeout, - self.retry_backoff, - self.max_sleep, - self.max_retries, ) if verify_ssl is not None: if isinstance(verify_ssl, bool): @@ -207,19 +156,10 @@ def tune( self.verify_ssl = str(verify_ssl) if http_timeout is not None: self.http_timeout = http_timeout - if retry_backoff is not None: - self.retry_backoff = retry_backoff - if max_sleep is not None: - self.max_sleep = max_sleep - if max_retries is not None: - self.max_retries = max_retries yield ( self.verify_ssl, self.http_timeout, - self.retry_backoff, - self.max_sleep, - self.max_retries, ) = saved_settings def _encode( @@ -259,7 +199,9 @@ def _set_authz_header( else: req.headers.pop("Authorization", None) # remove any possible value - def _retry_sleep(self, ctx: RetryContext) -> None: + def _retry_sleep( + self, retry_configuration: RetryConfiguration, ctx: RetryContext + ) -> None: """ Given a retry context, compute the amount of time to sleep and sleep that much This is always the minimum of the backoff (run on the context) and the @@ -268,8 +210,14 @@ def _retry_sleep(self, ctx: RetryContext) -> None: :param ctx: The context object which describes the state of the request and the retries which may already have been attempted. """ - sleep_period = min(self.retry_backoff(ctx), self.max_sleep) - log.debug("request retry_sleep(%s) [max=%s]", sleep_period, self.max_sleep) + sleep_period = min( + retry_configuration.backoff(ctx), retry_configuration.max_sleep + ) + log.debug( + "request retry_sleep(%s) [max=%s]", + sleep_period, + retry_configuration.max_sleep, + ) time.sleep(sleep_period) def request( @@ -291,7 +239,7 @@ def request( :param url: URL for the request :param method: HTTP request method, as an all caps string :param caller_info: Contextual information about the caller of the request, - including the authorizer. + including the authorizer and retry configuration. :param query_params: Parameters to be encoded as a query string :param headers: HTTP headers to add to the request :param data: Data to send as the request body. May pass through encoding. @@ -309,10 +257,11 @@ def request( log.debug("starting request for %s", url) resp: requests.Response | None = None req = self._encode(method, url, query_params, data, headers, encoding) - checker = RetryCheckRunner(caller_info.retry_checks.checks) + retry_configuration = caller_info.retry_configuration + checker = RetryCheckRunner(retry_configuration.checks) log.debug("transport request state initialized") - for attempt in range(self.max_retries + 1): + for attempt in range(retry_configuration.max_retries + 1): log.debug("transport request retry cycle. attempt=%d", attempt) # add Authorization header, or (if it's a NullAuthorizer) possibly # explicitly remove the Authorization header @@ -332,7 +281,10 @@ def request( except requests.RequestException as err: log.debug("request hit error (RequestException)") ctx.exception = err - if attempt >= self.max_retries or not checker.should_retry(ctx): + if ( + attempt >= retry_configuration.max_retries + or not checker.should_retry(ctx) + ): log.warning("request done (fail, error)") raise exc.convert_request_exception(err) log.debug("request may retry (should-retry=true)") @@ -344,9 +296,9 @@ def request( log.debug("request may retry, will check attempts") # the request will be retried, so sleep... - if attempt < self.max_retries: + if attempt < retry_configuration.max_retries: log.debug("under attempt limit, will sleep") - self._retry_sleep(ctx) + self._retry_sleep(retry_configuration, ctx) if resp is None: raise ValueError("Somehow, retries ended without a response") log.warning("request reached max retries, done (fail, response)") diff --git a/src/globus_sdk/transport/retry.py b/src/globus_sdk/transport/retry.py index 439abe79f..227d33233 100644 --- a/src/globus_sdk/transport/retry.py +++ b/src/globus_sdk/transport/retry.py @@ -1,7 +1,6 @@ from __future__ import annotations import enum -import logging import typing as t import requests @@ -9,11 +8,13 @@ if t.TYPE_CHECKING: from .caller_info import RequestCallerInfo -log = logging.getLogger(__name__) - C = t.TypeVar("C", bound=t.Callable[..., t.Any]) +# alias useful for declaring retry-related types +RetryCheck = t.Callable[["RetryContext"], "RetryCheckResult"] + + class RetryContext: """ The RetryContext is an object passed to retry checks in order to determine whether @@ -90,65 +91,6 @@ def decorator(func: C) -> C: return decorator -# types useful for declaring RetryCheckRunner and related types -RetryCheck = t.Callable[[RetryContext], RetryCheckResult] - - -class RetryCheckRunner: - """ - A RetryCheckRunner is an object responsible for running retry checks over the - lifetime of a request. Unlike the checks or the retry context, the runner persists - between retries. It can therefore implement special logic for checks like "only try - this check once". - - Its primary responsibility is to answer the question "should_retry(context)?" with a - boolean. - - It takes as its input a list of checks. Checks may be paired with flags to indicate - their configuration options. When not paired with flags, the flags are taken to be - "NONE". - - Supported flags: - - ``RUN_ONCE`` - The check will run at most once for a given request. Once it has run, it is - recorded as "has_run" and will not be run again on that request. - """ - - # check configs: a list of pairs, (check, flags) - # a check without flags is assumed to have flags=NONE - def __init__(self, checks: t.Iterable[RetryCheck]) -> None: - self._checks: list[RetryCheck] = [] - self._check_data: dict[RetryCheck, dict[str, t.Any]] = {} - for check in checks: - self._checks.append(check) - self._check_data[check] = {} - - def should_retry(self, context: RetryContext) -> bool: - for check in self._checks: - flags = getattr(check, "_retry_check_flags", RetryCheckFlags.NONE) - - if flags & RetryCheckFlags.RUN_ONCE: - if self._check_data[check].get("has_run"): - continue - else: - self._check_data[check]["has_run"] = True - - result = check(context) - log.debug( # try to get name but don't fail if it's not a function... - "ran retry check (%s) => %s", getattr(check, "__name__", check), result - ) - if result is RetryCheckResult.no_decision: - continue - elif result is RetryCheckResult.do_not_retry: - return False - else: - return True - - # fallthrough: don't retry any request which isn't marked for retry - return False - - class RetryCheckCollection: """ A RetryCheckCollection is an ordered collection of retry checks which are @@ -160,7 +102,7 @@ class RetryCheckCollection: (except via the backoff which may be set) - what kinds of request parameters (e.g., timeouts) are used - It *only* contains `RetryCheck` methods which can look at a response or + It *only* contains ``RetryCheck`` methods which can look at a response or error and decide whether or not to retry. """ diff --git a/src/globus_sdk/transport/retry_check_runner.py b/src/globus_sdk/transport/retry_check_runner.py new file mode 100644 index 000000000..4d59edc58 --- /dev/null +++ b/src/globus_sdk/transport/retry_check_runner.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import logging +import typing as t + +from .retry import RetryCheck, RetryCheckFlags, RetryCheckResult, RetryContext + +log = logging.getLogger(__name__) + + +class RetryCheckRunner: + """ + A RetryCheckRunner is an object responsible for running retry checks over the + lifetime of a request. Unlike the checks or the retry context, the runner persists + between retries. It can therefore implement special logic for checks like "only try + this check once". + + Its primary responsibility is to answer the question "should_retry(context)?" with a + boolean. + + It takes as its input a list of checks. Checks may be paired with flags to indicate + their configuration options. When not paired with flags, the flags are taken to be + "NONE". + + Supported flags: + + ``RUN_ONCE`` + The check will run at most once for a given request. Once it has run, it is + recorded as "has_run" and will not be run again on that request. + """ + + # check configs: a list of pairs, (check, flags) + # a check without flags is assumed to have flags=NONE + def __init__(self, checks: t.Iterable[RetryCheck]) -> None: + self._checks: list[RetryCheck] = [] + self._check_data: dict[RetryCheck, dict[str, t.Any]] = {} + for check in checks: + self._checks.append(check) + self._check_data[check] = {} + + def should_retry(self, context: RetryContext) -> bool: + for check in self._checks: + flags = getattr(check, "_retry_check_flags", RetryCheckFlags.NONE) + + if flags & RetryCheckFlags.RUN_ONCE: + if self._check_data[check].get("has_run"): + continue + else: + self._check_data[check]["has_run"] = True + + result = check(context) + log.debug( # try to get name but don't fail if it's not a function... + "ran retry check (%s) => %s", getattr(check, "__name__", check), result + ) + if result is RetryCheckResult.no_decision: + continue + elif result is RetryCheckResult.do_not_retry: + return False + else: + return True + + # fallthrough: don't retry any request which isn't marked for retry + return False diff --git a/src/globus_sdk/transport/retry_config.py b/src/globus_sdk/transport/retry_config.py new file mode 100644 index 000000000..3f3398d96 --- /dev/null +++ b/src/globus_sdk/transport/retry_config.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import contextlib +import dataclasses +import random +import typing as t + +from .retry import RetryCheckCollection, RetryContext + + +def _exponential_backoff(ctx: RetryContext) -> float: + # respect any explicit backoff set on the context + if ctx.backoff is not None: + return ctx.backoff + # exponential backoff with jitter + return t.cast(float, (0.25 + 0.5 * random.random()) * (2**ctx.attempt)) + + +@dataclasses.dataclass +class RetryConfiguration: + """ + Configuration for a client which is going to retry requests. + + :param max_retries: The maximum number of retries allowed. + :param max_sleep: The maximum sleep time between retries (in seconds). If the + computed sleep time or the backoff requested by a retry check exceeds this + value, this amount of time will be used instead. + :param retry_backoff: A function which determines how long to sleep between calls + based on the RetryContext. Defaults to exponential backoff with jitter based on + the context ``attempt`` number. + :param retry_after_status_codes: HTTP status codes for responses which may have + a Retry-After header. + :param transient_error_status_codes: HTTP status codes for error responses which + should generally be retried. + :param expired_authorization_status_codes: HTTP status codes indicating that + authorization info was missing or expired. + :param checks: The check callbacks which will run in order to evaluate + responses and exceptions, as a ``RetryCheckCollection``. + """ + + checks: RetryCheckCollection + + max_retries: int = 5 + max_sleep: float | int = 10 + backoff: t.Callable[[RetryContext], float] = _exponential_backoff + retry_after_status_codes: tuple[int, ...] = (429, 503) + transient_error_status_codes: tuple[int, ...] = (429, 500, 502, 503, 504) + expired_authorization_status_codes: tuple[int, ...] = (401,) + + @contextlib.contextmanager + def tune( + self, + *, + backoff: t.Callable[[RetryContext], float] | None = None, + max_sleep: float | int | None = None, + max_retries: int | None = None, + ) -> t.Iterator[None]: + """ + Temporarily adjust some of the request retry settings. + This method works as a context manager, and will reset settings to their + original values after it exits. + + :param backoff: A function which determines how long to sleep between + calls based on the RetryContext + :param max_sleep: The maximum sleep time between retries (in seconds). If the + computed sleep time or the backoff requested by a retry check exceeds this + value, this amount of time will be used instead + :param max_retries: The maximum number of retries allowed by this transport + + **Example Usage** + + This can be used with any client class to temporarily set values in the context + of one or more HTTP requests. For example, to disable retries: + + >>> client = ... # any client class + >>> with client.retry_config.tune(max_retries=0): + >>> foo = client.get_foo() + + See also: :meth:`RequestsTransport.tune`. + """ + saved_settings = ( + self.backoff, + self.max_sleep, + self.max_retries, + ) + if backoff is not None: + self.backoff = backoff + if max_sleep is not None: + self.max_sleep = max_sleep + if max_retries is not None: + self.max_retries = max_retries + yield + ( + self.backoff, + self.max_sleep, + self.max_retries, + ) = saved_settings diff --git a/tests/conftest.py b/tests/conftest.py index 3b346007e..33c0a4be1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ import responses import globus_sdk -from globus_sdk.transport import RequestsTransport @pytest.fixture(autouse=True) @@ -13,11 +12,6 @@ def mocksleep(): yield m -@pytest.fixture -def no_retry_transport(): - return RequestsTransport(max_retries=0) - - @pytest.fixture(autouse=True) def mocked_responses(): """ diff --git a/tests/functional/base_client/test_retry_behavior.py b/tests/functional/base_client/test_retry_behavior.py index 570746a04..f9758ba0b 100644 --- a/tests/functional/base_client/test_retry_behavior.py +++ b/tests/functional/base_client/test_retry_behavior.py @@ -38,7 +38,7 @@ def test_retry_disabled_via_tune(client, mocksleep): # the error is seen by the client (automatic retry does not hide it) with pytest.raises(globus_sdk.GlobusAPIError) as excinfo: - with client.transport.tune(max_retries=0): + with client.retry_configuration.tune(max_retries=0): client.get("/bar") assert excinfo.value.http_status == 500 @@ -96,7 +96,7 @@ def test_retry_limit(client, mocksleep, num_errors, expect_err): def test_transport_retry_limit(client, mocksleep): # this limit is a safety to protect against a bad policy causing infinite retries - client.transport.max_retries = 2 + client.retry_configuration.max_retries = 2 for _i in range(3): load_response( @@ -115,11 +115,11 @@ def test_transport_retry_limit(client, mocksleep): def test_bad_max_retries_causes_error(client): - # this test exploits the fact that we loop to (transport.max_retries + 1) in order + # this test exploits the fact that we loop to (max_retries + 1) in order # to ensure that no requests are ever sent # the transport should throw an error in this case, since it doesn't have a response # value to return - client.transport.max_retries = -1 + client.retry_configuration.max_retries = -1 with pytest.raises(ValueError): client.get("/bar") @@ -294,7 +294,7 @@ def handle_missing_authorization(self): authorizer = DummyAuthorizer() caller_info = RequestCallerInfo( - retry_checks=client.request_retry_checks, authorizer=authorizer + retry_configuration=client.retry_configuration, authorizer=authorizer ) # Test direct transport usage with caller_info diff --git a/tests/functional/services/auth/confidential_client/conftest.py b/tests/functional/services/auth/confidential_client/conftest.py index 0486e7dea..a3cc1bdf6 100644 --- a/tests/functional/services/auth/confidential_client/conftest.py +++ b/tests/functional/services/auth/confidential_client/conftest.py @@ -4,7 +4,9 @@ @pytest.fixture -def auth_client(no_retry_transport): - return globus_sdk.ConfidentialAppAuthClient( - "dummy_client_id", "dummy_client_secret", transport=no_retry_transport +def auth_client(): + client = globus_sdk.ConfidentialAppAuthClient( + "dummy_client_id", "dummy_client_secret" ) + with client.retry_configuration.tune(max_retries=0): + yield client diff --git a/tests/functional/services/auth/conftest.py b/tests/functional/services/auth/conftest.py index 549584ddf..a1ab53523 100644 --- a/tests/functional/services/auth/conftest.py +++ b/tests/functional/services/auth/conftest.py @@ -4,10 +4,14 @@ @pytest.fixture -def login_client(no_retry_transport): - return globus_sdk.AuthLoginClient(transport=no_retry_transport) +def login_client(): + client = globus_sdk.AuthLoginClient() + with client.retry_configuration.tune(max_retries=0): + yield client @pytest.fixture -def service_client(no_retry_transport): - return globus_sdk.AuthClient(transport=no_retry_transport) +def service_client(): + client = globus_sdk.AuthClient() + with client.retry_configuration.tune(max_retries=0): + yield client diff --git a/tests/functional/services/auth/native_client/conftest.py b/tests/functional/services/auth/native_client/conftest.py index b67ad4633..0182d0f81 100644 --- a/tests/functional/services/auth/native_client/conftest.py +++ b/tests/functional/services/auth/native_client/conftest.py @@ -4,7 +4,7 @@ @pytest.fixture -def auth_client(no_retry_transport): - return globus_sdk.NativeAppAuthClient( - "dummy_client_id", transport=no_retry_transport - ) +def auth_client(): + client = globus_sdk.NativeAppAuthClient("dummy_client_id") + with client.retry_configuration.tune(max_retries=0): + yield client diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index b819eecb2..50725ca87 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -13,19 +13,19 @@ @pytest.fixture -def native_client(no_retry_transport): - return globus_sdk.NativeAppAuthClient( - client_id=CLIENT_ID, transport=no_retry_transport - ) +def native_client(): + client = globus_sdk.NativeAppAuthClient(client_id=CLIENT_ID) + with client.retry_configuration.tune(max_retries=0): + yield client @pytest.fixture -def confidential_client(no_retry_transport): - return globus_sdk.ConfidentialAppAuthClient( - client_id=CLIENT_ID, - client_secret="SECRET_SECRET_HES_GOT_A_SECRET", - transport=no_retry_transport, +def confidential_client(): + client = globus_sdk.ConfidentialAppAuthClient( + client_id=CLIENT_ID, client_secret="SECRET_SECRET_HES_GOT_A_SECRET" ) + with client.retry_configuration.tune(max_retries=0): + yield client # build a nearly-diagonal matrix over diff --git a/tests/functional/services/compute/conftest.py b/tests/functional/services/compute/conftest.py index 604b9cbd2..f72944d17 100644 --- a/tests/functional/services/compute/conftest.py +++ b/tests/functional/services/compute/conftest.py @@ -4,10 +4,14 @@ @pytest.fixture -def compute_client_v2(no_retry_transport): - return globus_sdk.ComputeClientV2(transport=no_retry_transport) +def compute_client_v2(): + client = globus_sdk.ComputeClientV2() + with client.retry_configuration.tune(max_retries=0): + yield client @pytest.fixture -def compute_client_v3(no_retry_transport): - return globus_sdk.ComputeClientV3(transport=no_retry_transport) +def compute_client_v3(): + client = globus_sdk.ComputeClientV3() + with client.retry_configuration.tune(max_retries=0): + yield client diff --git a/tests/functional/services/flows/conftest.py b/tests/functional/services/flows/conftest.py index 0d7224e0b..91eaf5307 100644 --- a/tests/functional/services/flows/conftest.py +++ b/tests/functional/services/flows/conftest.py @@ -6,17 +6,17 @@ @pytest.fixture -def flows_client(no_retry_transport): - return globus_sdk.FlowsClient(transport=no_retry_transport) +def flows_client(): + client = globus_sdk.FlowsClient() + with client.retry_configuration.tune(max_retries=0): + yield client @pytest.fixture -def specific_flow_client_class( - no_retry_transport, -) -> t.Type[globus_sdk.SpecificFlowClient]: +def specific_flow_client_class() -> t.Type[globus_sdk.SpecificFlowClient]: class CustomSpecificFlowClient(globus_sdk.SpecificFlowClient): def __init__(self, **kwargs) -> None: - kwargs["transport"] = no_retry_transport super().__init__(**kwargs) + self.retry_configuration.max_retries = 0 return CustomSpecificFlowClient diff --git a/tests/functional/services/gcs/conftest.py b/tests/functional/services/gcs/conftest.py index 5a17a9515..c4900790f 100644 --- a/tests/functional/services/gcs/conftest.py +++ b/tests/functional/services/gcs/conftest.py @@ -4,6 +4,8 @@ @pytest.fixture -def client(no_retry_transport): +def client(): # default fqdn for GCS client testing - return globus_sdk.GCSClient("abc.xyz.data.globus.org", transport=no_retry_transport) + client = globus_sdk.GCSClient("abc.xyz.data.globus.org") + with client.retry_configuration.tune(max_retries=0): + yield client diff --git a/tests/functional/services/groups/conftest.py b/tests/functional/services/groups/conftest.py index eb5018ac4..7025ea1e9 100644 --- a/tests/functional/services/groups/conftest.py +++ b/tests/functional/services/groups/conftest.py @@ -4,8 +4,10 @@ @pytest.fixture -def groups_client(no_retry_transport): - return globus_sdk.GroupsClient(transport=no_retry_transport) +def groups_client(): + client = globus_sdk.GroupsClient() + with client.retry_configuration.tune(max_retries=0): + yield client @pytest.fixture diff --git a/tests/functional/services/search/conftest.py b/tests/functional/services/search/conftest.py index 24f496263..4ce9f9674 100644 --- a/tests/functional/services/search/conftest.py +++ b/tests/functional/services/search/conftest.py @@ -4,5 +4,7 @@ @pytest.fixture -def client(no_retry_transport): - return globus_sdk.SearchClient(transport=no_retry_transport) +def client(): + client = globus_sdk.SearchClient() + with client.retry_configuration.tune(max_retries=0): + yield client diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index a08269d16..7d5d2542d 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -12,8 +12,10 @@ @pytest.fixture -def search_client(no_retry_transport): - return globus_sdk.SearchClient(transport=no_retry_transport) +def search_client(): + client = globus_sdk.SearchClient() + with client.retry_configuration.tune(max_retries=0): + yield client def test_search_query_simple(search_client): diff --git a/tests/functional/services/search/test_search_roles.py b/tests/functional/services/search/test_search_roles.py index 8c2f13f72..e8ef9c4ea 100644 --- a/tests/functional/services/search/test_search_roles.py +++ b/tests/functional/services/search/test_search_roles.py @@ -7,8 +7,10 @@ @pytest.fixture -def search_client(no_retry_transport): - return globus_sdk.SearchClient(transport=no_retry_transport) +def search_client(): + client = globus_sdk.SearchClient() + with client.retry_configuration.tune(max_retries=0): + yield client def test_search_role_create(search_client): diff --git a/tests/functional/services/transfer/conftest.py b/tests/functional/services/transfer/conftest.py index 9bead524c..85b5935c8 100644 --- a/tests/functional/services/transfer/conftest.py +++ b/tests/functional/services/transfer/conftest.py @@ -4,5 +4,7 @@ @pytest.fixture -def client(no_retry_transport): - return globus_sdk.TransferClient(transport=no_retry_transport) +def client(): + client = globus_sdk.TransferClient() + with client.retry_configuration.tune(max_retries=0): + yield client diff --git a/tests/functional/tokenstorage/v2/conftest.py b/tests/functional/tokenstorage/v2/conftest.py index 52b42023c..0ba54a457 100644 --- a/tests/functional/tokenstorage/v2/conftest.py +++ b/tests/functional/tokenstorage/v2/conftest.py @@ -15,10 +15,10 @@ def id_token_sub(): @pytest.fixture -def cc_auth_client(no_retry_transport): - return globus_sdk.ConfidentialAppAuthClient( - "dummy_id", "dummy_secret", transport=no_retry_transport - ) +def cc_auth_client(): + client = globus_sdk.ConfidentialAppAuthClient("dummy_id", "dummy_secret") + with client.retry_configuration.tune(max_retries=0): + yield client @pytest.fixture diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index 691971c37..95ce97251 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -20,15 +20,15 @@ def auth_client(): @pytest.fixture -def base_client_class(no_retry_transport): +def base_client_class(): class CustomClient(globus_sdk.BaseClient): service_name = "transfer" scopes = TransferScopes default_scope_requirements = [TransferScopes.all] def __init__(self, **kwargs) -> None: - kwargs["transport"] = no_retry_transport super().__init__(**kwargs) + self.retry_configuration.max_retries = 0 return CustomClient diff --git a/tests/unit/transport/test_default_retry_policy.py b/tests/unit/transport/test_default_retry_policy.py index 3d4cc715d..41b7c974a 100644 --- a/tests/unit/transport/test_default_retry_policy.py +++ b/tests/unit/transport/test_default_retry_policy.py @@ -8,62 +8,63 @@ RequestsTransport, RetryCheckResult, RetryCheckRunner, + RetryConfiguration, RetryContext, ) @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_respects_retry_after(mocksleep, http_status): - retry_checks = DefaultRetryCheckCollection() + retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) transport = RequestsTransport() - checker = RetryCheckRunner(retry_checks) + checker = RetryCheckRunner(retry_config.checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "5"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(retry_checks=retry_checks) + caller_info = RequestCallerInfo(retry_configuration=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True mocksleep.assert_not_called() - transport._retry_sleep(ctx) + transport._retry_sleep(retry_config, ctx) mocksleep.assert_called_once_with(5) @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_ignores_retry_after_too_high(mocksleep, http_status): # set explicit max sleep to confirm that the value is capped here - transport = RequestsTransport(max_sleep=5) - retry_checks = DefaultRetryCheckCollection() - checker = RetryCheckRunner(retry_checks) + retry_config = RetryConfiguration(max_sleep=5, checks=DefaultRetryCheckCollection()) + transport = RequestsTransport() + checker = RetryCheckRunner(retry_config.checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "20"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(retry_checks=retry_checks) + caller_info = RequestCallerInfo(retry_configuration=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True mocksleep.assert_not_called() - transport._retry_sleep(ctx) + transport._retry_sleep(retry_config, ctx) mocksleep.assert_called_once_with(5) @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): + retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) transport = RequestsTransport() - retry_checks = DefaultRetryCheckCollection() - checker = RetryCheckRunner(retry_checks) + checker = RetryCheckRunner(retry_config.checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "not-an-integer"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(retry_checks=retry_checks) + caller_info = RequestCallerInfo(retry_configuration=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True mocksleep.assert_not_called() - transport._retry_sleep(ctx) + transport._retry_sleep(retry_config, ctx) mocksleep.assert_called_once() @@ -75,8 +76,8 @@ def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): ], ) def test_default_retry_check_noop_on_exception(checkname, mocksleep): - retry_checks = DefaultRetryCheckCollection() - method = getattr(retry_checks, checkname) - caller_info = RequestCallerInfo(retry_checks=retry_checks) + retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) + method = getattr(retry_config.checks, checkname) + caller_info = RequestCallerInfo(retry_configuration=retry_config) ctx = RetryContext(1, caller_info=caller_info, exception=Exception("foo")) assert method(ctx) is RetryCheckResult.no_decision diff --git a/tests/unit/transport/test_retry_check_runner.py b/tests/unit/transport/test_retry_check_runner.py index 6932963cd..e70283fa9 100644 --- a/tests/unit/transport/test_retry_check_runner.py +++ b/tests/unit/transport/test_retry_check_runner.py @@ -5,12 +5,14 @@ RequestCallerInfo, RetryCheckResult, RetryCheckRunner, + RetryConfiguration, RetryContext, ) def _make_test_retry_context(*, status=200, exception=None, response=None): - caller_info = RequestCallerInfo(retry_checks=DefaultRetryCheckCollection()) + retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) + caller_info = RequestCallerInfo(retry_configuration=retry_config) if exception: return RetryContext(1, caller_info=caller_info, exception=exception) elif response: diff --git a/tests/unit/transport/test_transfer_transport.py b/tests/unit/transport/test_transfer_transport.py index 0f98e063f..44260c64a 100644 --- a/tests/unit/transport/test_transfer_transport.py +++ b/tests/unit/transport/test_transfer_transport.py @@ -1,12 +1,17 @@ from unittest import mock from globus_sdk.services.transfer.transport import TransferDefaultRetryCheckCollection -from globus_sdk.transport import RequestCallerInfo, RetryCheckRunner, RetryContext +from globus_sdk.transport import ( + RequestCallerInfo, + RetryCheckRunner, + RetryConfiguration, + RetryContext, +) def test_transfer_does_not_retry_external(): - retry_checks = TransferDefaultRetryCheckCollection() - checker = RetryCheckRunner(retry_checks) + retry_config = RetryConfiguration(checks=TransferDefaultRetryCheckCollection()) + checker = RetryCheckRunner(retry_config.checks) body = { "HTTP status": "502", @@ -19,15 +24,15 @@ def test_transfer_does_not_retry_external(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - caller_info = RequestCallerInfo(retry_checks=retry_checks) + caller_info = RequestCallerInfo(retry_configuration=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False def test_transfer_does_not_retry_endpoint_error(): - retry_checks = TransferDefaultRetryCheckCollection() - checker = RetryCheckRunner(retry_checks) + retry_config = RetryConfiguration(checks=TransferDefaultRetryCheckCollection()) + checker = RetryCheckRunner(retry_config.checks) body = { "HTTP status": "502", @@ -43,15 +48,15 @@ def test_transfer_does_not_retry_endpoint_error(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - caller_info = RequestCallerInfo(retry_checks=retry_checks) + caller_info = RequestCallerInfo(retry_configuration=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False def test_transfer_retries_others(): - retry_checks = TransferDefaultRetryCheckCollection() - checker = RetryCheckRunner(retry_checks) + retry_config = RetryConfiguration(checks=TransferDefaultRetryCheckCollection()) + checker = RetryCheckRunner(retry_config.checks) def _raise_value_error(): raise ValueError() @@ -59,7 +64,7 @@ def _raise_value_error(): dummy_response = mock.Mock() dummy_response.json = _raise_value_error dummy_response.status_code = 502 - caller_info = RequestCallerInfo(retry_checks=retry_checks) + caller_info = RequestCallerInfo(retry_configuration=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True diff --git a/tests/unit/transport/test_transport.py b/tests/unit/transport/test_transport.py index d204acc1e..ff888328f 100644 --- a/tests/unit/transport/test_transport.py +++ b/tests/unit/transport/test_transport.py @@ -3,8 +3,13 @@ import pytest -from globus_sdk.transport import RequestsTransport, RetryContext -from globus_sdk.transport.requests import _exponential_backoff +from globus_sdk.transport import ( + DefaultRetryCheckCollection, + RequestsTransport, + RetryConfiguration, + RetryContext, +) +from globus_sdk.transport.retry_config import _exponential_backoff def _linear_backoff(ctx: RetryContext) -> float: @@ -30,11 +35,6 @@ def _linear_backoff(ctx: RetryContext) -> float: ("verify_ssl", True, ca_bundle_non_existent), ("verify_ssl", True, str(ca_bundle_non_existent)), ("http_timeout", 60, 120), - ("retry_backoff", _exponential_backoff, _linear_backoff), - ("max_sleep", 10, 10), - ("max_sleep", 10, 1), - ("max_retries", 0, 5), - ("max_retries", 10, 0), ], ) def test_transport_tuning(param_name, init_value, tune_value): @@ -53,6 +53,29 @@ def test_transport_tuning(param_name, init_value, tune_value): assert getattr(transport, param_name) == init_value +@pytest.mark.parametrize( + "param_name, init_value, tune_value", + [ + ("backoff", _exponential_backoff, _linear_backoff), + ("max_sleep", 10, 10), + ("max_sleep", 10, 1), + ("max_retries", 0, 5), + ("max_retries", 10, 0), + ], +) +def test_retry_tuning(param_name, init_value, tune_value): + init_kwargs = {param_name: init_value} + config = RetryConfiguration(DefaultRetryCheckCollection(), **init_kwargs) + + assert getattr(config, param_name) == init_value + + tune_kwargs = {param_name: tune_value} + with config.tune(**tune_kwargs): + assert getattr(config, param_name) == tune_value + + assert getattr(config, param_name) == init_value + + def test_transport_can_manipulate_user_agent(): transport = RequestsTransport() diff --git a/tests/unit/transport/test_transport_authz_handling.py b/tests/unit/transport/test_transport_authz_handling.py index 9847fbf49..458cdf203 100644 --- a/tests/unit/transport/test_transport_authz_handling.py +++ b/tests/unit/transport/test_transport_authz_handling.py @@ -7,6 +7,7 @@ DefaultRetryCheckCollection, RequestCallerInfo, RequestsTransport, + RetryConfiguration, ) @@ -37,11 +38,12 @@ def test_will_null_authz_header_with_null_authorizer(): def test_requests_transport_accepts_caller_info(): + retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) transport = RequestsTransport() mock_authorizer = mock.Mock() mock_authorizer.get_authorization_header.return_value = "Bearer token" caller_info = RequestCallerInfo( - retry_checks=DefaultRetryCheckCollection(), authorizer=mock_authorizer + retry_configuration=retry_config, authorizer=mock_authorizer ) with mock.patch.object(transport, "session") as mock_session: @@ -66,8 +68,9 @@ def test_requests_transport_caller_info_required(): def test_requests_transport_keyword_only(): + retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) transport = RequestsTransport() - caller_info = RequestCallerInfo(retry_checks=DefaultRetryCheckCollection()) + caller_info = RequestCallerInfo(retry_configuration=retry_config) with pytest.raises(TypeError): transport.request("GET", "https://example.com", caller_info) From 4db7bcce012918ee45d3b7100effd77fd1dff5b1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 21 Jul 2025 20:35:45 -0500 Subject: [PATCH 155/176] Add a changelog for RequestsTransport refactoring --- ...sirosen_change_transport_passing_style.rst | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst diff --git a/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst b/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst new file mode 100644 index 000000000..71f2ade75 --- /dev/null +++ b/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst @@ -0,0 +1,25 @@ +Breaking Changes +---------------- + +- The ``RequestsTransport`` object has been refactored to separate it from + configuration which controls request retries. A new ``RetryConfiguration`` + object is introduced and provided as ``client.retry_configuration`` 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``, + instead of ``transport_params``. + + - Users seeking to customize the retry backoff, sleep maximum, and max + retries should now use ``retry_configuration``, as these are no longer + controlled through ``transport``. + + - The capabilities of the ``RequestsTransport.tune()`` context manager have + been divided between ``RequestsTransport.tune()`` and + ``RetryConfiguration.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. From 184d1c717d8d2c1d6b65def4310fc6a067f481c8 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 22 Jul 2025 14:23:06 -0500 Subject: [PATCH 156/176] Refactor RetryConfig to isolate hook registration In order to give TransferClient control over its retry checks but still have the retry config exposed to user control, hook registration is moved off of the core models for retry config and instead now lives in a callback in the client definitions. The base client registers the default hooks (which are just provided as a tuple). The TransferClient registers its own modified suite of hooks. Additionally: - rename to 'retry_config / RetryConfig' to match 'GlobusAppConfig' - clean up some tests to use simpler call styles - add some missing functional tests of transfer retries - update the RetryConfig docs --- ...sirosen_change_transport_passing_style.rst | 20 +- docs/upgrading.rst | 28 +-- src/globus_sdk/client.py | 30 ++- .../services/auth/client/base_login_client.py | 4 +- .../auth/client/confidential_client.py | 4 +- .../services/auth/client/native_client.py | 4 +- .../services/auth/client/service_client.py | 4 +- src/globus_sdk/services/flows/client.py | 4 +- src/globus_sdk/services/gcs/client.py | 4 +- src/globus_sdk/services/transfer/client.py | 7 +- src/globus_sdk/services/transfer/transport.py | 68 ++++--- src/globus_sdk/transport/__init__.py | 6 +- src/globus_sdk/transport/caller_info.py | 6 +- .../transport/default_retry_checks.py | 177 ++++++++-------- src/globus_sdk/transport/requests.py | 29 +-- src/globus_sdk/transport/retry.py | 20 +- src/globus_sdk/transport/retry_config.py | 10 +- tests/functional/base_client/conftest.py | 9 +- .../base_client/test_retry_behavior.py | 191 ++++++++---------- .../auth/confidential_client/conftest.py | 2 +- tests/functional/services/auth/conftest.py | 4 +- .../services/auth/native_client/conftest.py | 2 +- .../services/auth/test_auth_client_flow.py | 4 +- tests/functional/services/compute/conftest.py | 4 +- tests/functional/services/flows/conftest.py | 4 +- tests/functional/services/gcs/conftest.py | 2 +- tests/functional/services/groups/conftest.py | 2 +- tests/functional/services/search/conftest.py | 2 +- .../functional/services/search/test_search.py | 2 +- .../services/search/test_search_roles.py | 2 +- .../functional/services/transfer/conftest.py | 2 +- .../transfer/test_custom_retry_behavior.py | 49 +++++ tests/functional/tokenstorage/v2/conftest.py | 2 +- .../sphinxext/test_copyparams_directive.py | 1 + tests/unit/test_base_client.py | 2 +- .../transport/test_default_retry_policy.py | 41 ++-- .../unit/transport/test_retry_check_runner.py | 9 +- .../unit/transport/test_transfer_transport.py | 37 +++- tests/unit/transport/test_transport.py | 9 +- .../test_transport_authz_handling.py | 15 +- 40 files changed, 455 insertions(+), 367 deletions(-) create mode 100644 tests/functional/services/transfer/test_custom_retry_behavior.py diff --git a/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst b/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst index 71f2ade75..adb36bb0f 100644 --- a/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst +++ b/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst @@ -2,23 +2,23 @@ Breaking Changes ---------------- - The ``RequestsTransport`` object has been refactored to separate it from - configuration which controls request retries. A new ``RetryConfiguration`` - object is introduced and provided as ``client.retry_configuration`` on - all client types. The interface for controlling these configurations has been - updated. (:pr:`NUMBER`) + 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``, - instead of ``transport_params``. + - 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_configuration``, as these are no longer - controlled through ``transport``. + 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 between ``RequestsTransport.tune()`` and - ``RetryConfiguration.tune()``. + 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 diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 03ce2c20f..4cf493811 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -474,12 +474,6 @@ or use the ``tune()`` context manager: .. code-block:: python - # globus-sdk v3 - import globus_sdk - - client = globus_sdk.GroupsClient(transport_params={"http_timeout": 120.0}) - my_groups = client.get_my_groups() - # globus-sdk v4 import globus_sdk @@ -487,15 +481,15 @@ or use the ``tune()`` context manager: with client.transport.tune(http_timeout=120.0): my_groups = client.get_my_groups() -Retry Check Configuration Moved to ``retry_configuration`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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_configuration``. +attribute of the client, ``retry_config``. These changes primarily impact users who were using a custom ``RequestsTransport`` class, and should simplify their usage. @@ -522,7 +516,7 @@ This could then be configured on a custom client class: client = MyClientClass() Under SDK v4, in order to customize the same information, users can simply -client and then modify the attributes of the ``retry_configuration`` object: +client and then modify the attributes of the ``retry_config`` object: .. code-block:: python @@ -530,7 +524,7 @@ client and then modify the attributes of the ``retry_configuration`` object: import globus_sdk client = globus_sdk.GroupsClient() - client.retry_configuration.transient_error_status_codes = (502,) + 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()`` @@ -544,9 +538,19 @@ For example, users can suppress retries: import globus_sdk client = globus_sdk.GroupsClient() - with client.retry_configuration.tune(max_retries=1): + 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 diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index 5dcf26d29..75e2a31dd 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -12,12 +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 ( - DefaultRetryCheckCollection, - RequestCallerInfo, - RequestsTransport, - RetryConfiguration, -) +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 @@ -55,11 +51,9 @@ class BaseClient: attribute of the same name. :param transport: A :class:`RequestsTransport` object for sending and retrying requests. By default, one will be constructed by the client. - - :ivar RetryCheckCollection retry_configuration: The retry configuration for a - given client. This determines which requests will be retried on failure, - how many retries will be attempted, and how long the SDK may wait - between retries. + :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 @@ -87,6 +81,7 @@ def __init__( authorizer: GlobusAuthorizer | None = None, app_name: str | None = None, transport: RequestsTransport | None = None, + retry_config: RetryConfig | None = None, ) -> None: # check for input parameter conflicts if app_scopes and not app: @@ -113,7 +108,9 @@ 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.retry_configuration: RetryConfiguration = self._get_default_retry_config() + 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)}") @@ -146,13 +143,13 @@ def default_scope_requirements(self) -> list[Scope]: """ raise NotImplementedError - def _get_default_retry_config(self) -> RetryConfiguration: + def _register_standard_retry_checks(self, retry_config: RetryConfig) -> None: """ - Create the default retry configuration. + Setup the standard checks for this client. This is called during init and may be overridden by subclasses. """ - return RetryConfiguration(checks=DefaultRetryCheckCollection()) + retry_config.checks.register_many_checks(DEFAULT_RETRY_CHECKS) @classmethod def _resolve_base_url(cls, init_base_url: str | None, environment: str) -> str: @@ -513,8 +510,7 @@ def request( # capture info about this client as the caller to pass to the transport caller_info = RequestCallerInfo( - retry_configuration=self.retry_configuration, - authorizer=authorizer, + retry_config=self.retry_config, authorizer=authorizer ) # make the request diff --git a/src/globus_sdk/services/auth/client/base_login_client.py b/src/globus_sdk/services/auth/client/base_login_client.py index 65a86bb48..34ce44b1a 100644 --- a/src/globus_sdk/services/auth/client/base_login_client.py +++ b/src/globus_sdk/services/auth/client/base_login_client.py @@ -12,7 +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 +from globus_sdk.transport import RequestsTransport, RetryConfig from .._common import get_jwk_data, pem_decode_jwk_data from ..errors import AuthAPIError @@ -54,6 +54,7 @@ def __init__( authorizer: GlobusAuthorizer | None = None, app_name: str | None = None, transport: RequestsTransport | None = None, + retry_config: RetryConfig | None = None, ) -> None: super().__init__( environment=environment, @@ -61,6 +62,7 @@ def __init__( authorizer=authorizer, app_name=app_name, 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 diff --git a/src/globus_sdk/services/auth/client/confidential_client.py b/src/globus_sdk/services/auth/client/confidential_client.py index 41c35627c..c3f160986 100644 --- a/src/globus_sdk/services/auth/client/confidential_client.py +++ b/src/globus_sdk/services/auth/client/confidential_client.py @@ -10,7 +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 +from globus_sdk.transport import RequestsTransport, RetryConfig from ..flow_managers import GlobusAuthorizationCodeFlowManager from ..response import OAuthClientCredentialsResponse, OAuthDependentTokenResponse @@ -49,6 +49,7 @@ def __init__( base_url: str | None = None, app_name: str | None = None, transport: RequestsTransport | None = None, + retry_config: RetryConfig | None = None, ) -> None: super().__init__( client_id=client_id, @@ -57,6 +58,7 @@ def __init__( base_url=base_url, app_name=app_name, transport=transport, + retry_config=retry_config, ) def oauth2_client_credentials_tokens( diff --git a/src/globus_sdk/services/auth/client/native_client.py b/src/globus_sdk/services/auth/client/native_client.py index 364413534..3c23d0a22 100644 --- a/src/globus_sdk/services/auth/client/native_client.py +++ b/src/globus_sdk/services/auth/client/native_client.py @@ -8,7 +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 +from globus_sdk.transport import RequestsTransport, RetryConfig from ..flow_managers import GlobusNativeAppFlowManager from ..response import OAuthRefreshTokenResponse @@ -41,6 +41,7 @@ def __init__( base_url: str | None = None, app_name: str | None = None, transport: RequestsTransport | None = None, + retry_config: RetryConfig | None = None, ) -> None: super().__init__( client_id=client_id, @@ -49,6 +50,7 @@ def __init__( base_url=base_url, app_name=app_name, transport=transport, + retry_config=retry_config, ) def oauth2_start_flow( diff --git a/src/globus_sdk/services/auth/client/service_client.py b/src/globus_sdk/services/auth/client/service_client.py index ed3c3b633..7e4e8d77d 100644 --- a/src/globus_sdk/services/auth/client/service_client.py +++ b/src/globus_sdk/services/auth/client/service_client.py @@ -12,7 +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 +from globus_sdk.transport import RequestsTransport, RetryConfig if t.TYPE_CHECKING: from globus_sdk.globus_app import GlobusApp @@ -80,6 +80,7 @@ def __init__( authorizer: GlobusAuthorizer | None = None, app_name: str | None = None, transport: RequestsTransport | None = None, + retry_config: RetryConfig | None = None, ) -> None: super().__init__( environment=environment, @@ -89,6 +90,7 @@ def __init__( authorizer=authorizer, app_name=app_name, transport=transport, + retry_config=retry_config, ) # FYI: this get_openid_configuration method is duplicated in AuthLoginBaseClient diff --git a/src/globus_sdk/services/flows/client.py b/src/globus_sdk/services/flows/client.py index 864a657e5..2dbead5d1 100644 --- a/src/globus_sdk/services/flows/client.py +++ b/src/globus_sdk/services/flows/client.py @@ -18,7 +18,7 @@ SpecificFlowScopes, TransferScopes, ) -from globus_sdk.transport import RequestsTransport +from globus_sdk.transport import RequestsTransport, RetryConfig from .data import RunActivityNotificationPolicy from .errors import FlowsAPIError @@ -910,6 +910,7 @@ def __init__( authorizer: GlobusAuthorizer | None = None, app_name: str | None = None, transport: RequestsTransport | None = None, + retry_config: RetryConfig | None = None, ) -> None: self._flow_id = flow_id self.scopes = SpecificFlowScopes(flow_id) @@ -920,6 +921,7 @@ def __init__( authorizer=authorizer, app_name=app_name, transport=transport, + retry_config=retry_config, ) @property diff --git a/src/globus_sdk/services/gcs/client.py b/src/globus_sdk/services/gcs/client.py index e6d43399d..e1ea8a6c1 100644 --- a/src/globus_sdk/services/gcs/client.py +++ b/src/globus_sdk/services/gcs/client.py @@ -11,7 +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 +from globus_sdk.transport import RequestsTransport, RetryConfig from .data import ( CollectionDocument, @@ -58,6 +58,7 @@ def __init__( authorizer: GlobusAuthorizer | None = None, app_name: str | 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://"): @@ -78,6 +79,7 @@ def __init__( authorizer=authorizer, app_name=app_name, transport=transport, + retry_config=retry_config, ) @staticmethod diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index d4ff48c4b..36300ee14 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -11,11 +11,12 @@ from globus_sdk._internal.type_definitions import DateLike, IntLike from globus_sdk._missing import MISSING, MissingType from globus_sdk.scopes import GCSCollectionScopes, Scope, TransferScopes +from globus_sdk.transport import RetryConfig from .data import DeleteData, TransferData from .errors import TransferAPIError from .response import IterableTransferResponse -from .transport import TransferDefaultRetryCheckCollection +from .transport import TRANSFER_DEFAULT_RETRY_CHECKS log = logging.getLogger(__name__) @@ -133,9 +134,9 @@ class TransferClient(client.BaseClient): scopes = TransferScopes default_scope_requirements = [TransferScopes.all] - def _get_default_retry_checks(self) -> TransferDefaultRetryCheckCollection: + def _register_standard_retry_checks(self, retry_config: RetryConfig) -> None: """Override the default retry checks.""" - return TransferDefaultRetryCheckCollection() + retry_config.checks.register_many_checks(TRANSFER_DEFAULT_RETRY_CHECKS) def add_app_data_access_scope( self, collection_ids: uuid.UUID | str | t.Iterable[uuid.UUID | str] diff --git a/src/globus_sdk/services/transfer/transport.py b/src/globus_sdk/services/transfer/transport.py index ef35d328e..4ab2c6888 100644 --- a/src/globus_sdk/services/transfer/transport.py +++ b/src/globus_sdk/services/transfer/transport.py @@ -3,36 +3,44 @@ the default check_transient_error """ -from globus_sdk.transport import ( - DefaultRetryCheckCollection, - RetryCheckResult, - RetryContext, +from __future__ import annotations + +from globus_sdk.transport import RetryCheck, RetryCheckResult, RetryContext +from globus_sdk.transport.default_retry_checks import ( + DEFAULT_RETRY_CHECKS, + check_transient_error, ) -class TransferDefaultRetryCheckCollection(DefaultRetryCheckCollection): - def check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: - """ - check for transient error status codes which could be resolved by - retrying the request. Does not retry ExternalErrors or EndpointErrors - as those are unlikely to actually be transient. - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted - """ - retry_config = ctx.caller_info.retry_configuration - if ctx.response is not None and ( - ctx.response.status_code in retry_config.transient_error_status_codes - ): - try: - code = ctx.response.json()["code"] - except (ValueError, KeyError): - code = "" - - for non_retry_code in ("ExternalError", "EndpointError"): - if non_retry_code in code: - return RetryCheckResult.no_decision - - return RetryCheckResult.do_retry - - return RetryCheckResult.no_decision +def check_transfer_transient_error(ctx: RetryContext) -> RetryCheckResult: + """ + check for transient error status codes which could be resolved by + retrying the request. Does not retry ExternalErrors or EndpointErrors + as those are unlikely to actually be transient. + + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted + """ + retry_config = ctx.caller_info.retry_config + if ctx.response is not None and ( + ctx.response.status_code in retry_config.transient_error_status_codes + ): + try: + code = ctx.response.json()["code"] + except (ValueError, KeyError): + code = "" + + for non_retry_code in ("ExternalError", "EndpointError"): + if non_retry_code in code: + return RetryCheckResult.no_decision + + return RetryCheckResult.do_retry + + return RetryCheckResult.no_decision + + +# Transfer retry checks are the defaults with the transient error one replaced +TRANSFER_DEFAULT_RETRY_CHECKS: tuple[RetryCheck, ...] = tuple( + check_transfer_transient_error if check is check_transient_error else check + for check in DEFAULT_RETRY_CHECKS +) diff --git a/src/globus_sdk/transport/__init__.py b/src/globus_sdk/transport/__init__.py index 4e1141d77..d0cdd6bba 100644 --- a/src/globus_sdk/transport/__init__.py +++ b/src/globus_sdk/transport/__init__.py @@ -1,6 +1,5 @@ from ._clientinfo import GlobusClientInfo from .caller_info import RequestCallerInfo -from .default_retry_checks import DefaultRetryCheckCollection from .encoders import FormRequestEncoder, JSONRequestEncoder, RequestEncoder from .requests import RequestsTransport from .retry import ( @@ -12,7 +11,7 @@ set_retry_check_flags, ) from .retry_check_runner import RetryCheckRunner -from .retry_config import RetryConfiguration +from .retry_config import RetryConfig __all__ = ( "RequestsTransport", @@ -24,8 +23,7 @@ "RetryCheckRunner", "set_retry_check_flags", "RetryContext", - "RetryConfiguration", - "DefaultRetryCheckCollection", + "RetryConfig", "RequestEncoder", "JSONRequestEncoder", "FormRequestEncoder", diff --git a/src/globus_sdk/transport/caller_info.py b/src/globus_sdk/transport/caller_info.py index 0295e3bee..b48bc573e 100644 --- a/src/globus_sdk/transport/caller_info.py +++ b/src/globus_sdk/transport/caller_info.py @@ -2,7 +2,7 @@ from globus_sdk.authorizers import GlobusAuthorizer -from .retry_config import RetryConfiguration +from .retry_config import RetryConfig class RequestCallerInfo: @@ -16,8 +16,8 @@ class RequestCallerInfo: def __init__( self, *, - retry_configuration: RetryConfiguration, + retry_config: RetryConfig, authorizer: GlobusAuthorizer | None = None, ) -> None: self.authorizer = authorizer - self.retry_configuration = retry_configuration + self.retry_config = retry_config diff --git a/src/globus_sdk/transport/default_retry_checks.py b/src/globus_sdk/transport/default_retry_checks.py index b448a94c3..b9d1129f6 100644 --- a/src/globus_sdk/transport/default_retry_checks.py +++ b/src/globus_sdk/transport/default_retry_checks.py @@ -3,7 +3,7 @@ import requests from .retry import ( - RetryCheckCollection, + RetryCheck, RetryCheckFlags, RetryCheckResult, RetryContext, @@ -11,100 +11,101 @@ ) -class DefaultRetryCheckCollection(RetryCheckCollection): - """The default checks for the SDK.""" +def check_request_exception(ctx: RetryContext) -> RetryCheckResult: + """ + Check if a network error was encountered - def __init__(self) -> None: - super().__init__() + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + if ctx.exception and isinstance(ctx.exception, requests.RequestException): + return RetryCheckResult.do_retry + return RetryCheckResult.no_decision - self.register_check(self.check_expired_authorization) - self.register_check(self.check_request_exception) - self.register_check(self.check_retry_after_header) - self.register_check(self.check_transient_error) - def check_request_exception(self, ctx: RetryContext) -> RetryCheckResult: - """ - Check if a network error was encountered +def check_retry_after_header(ctx: RetryContext) -> RetryCheckResult: + """ + Check for a retry-after header if the response had a matching status - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - if ctx.exception and isinstance(ctx.exception, requests.RequestException): - return RetryCheckResult.do_retry + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + retry_config = ctx.caller_info.retry_config + if ctx.response is None or ( + ctx.response.status_code not in retry_config.retry_after_status_codes + ): + return RetryCheckResult.no_decision + retry_after = _parse_retry_after(ctx.response) + if retry_after: + ctx.backoff = float(retry_after) + return RetryCheckResult.do_retry + + +def check_transient_error(ctx: RetryContext) -> RetryCheckResult: + """ + Check for transient error status codes which could be resolved by retrying + the request + + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + retry_config = ctx.caller_info.retry_config + if ctx.response is not None and ( + ctx.response.status_code in retry_config.transient_error_status_codes + ): + return RetryCheckResult.do_retry + return RetryCheckResult.no_decision + + +@set_retry_check_flags(RetryCheckFlags.RUN_ONCE) +def check_expired_authorization(ctx: RetryContext) -> RetryCheckResult: + """ + This check evaluates whether or not there is invalid or expired authorization + information which could be updated with some action -- most typically a token + refresh for an expired access token. + + The check is flagged to only run once per request. + + :param ctx: The context object which describes the state of the request and the + retries which may already have been attempted. + """ + retry_config = ctx.caller_info.retry_config + if ( # is the current check applicable? + ctx.response is None + or ctx.caller_info is None + or ctx.caller_info.authorizer is None + or ( + ctx.response.status_code + not in retry_config.expired_authorization_status_codes + ) + ): return RetryCheckResult.no_decision - def check_retry_after_header(self, ctx: RetryContext) -> RetryCheckResult: - """ - Check for a retry-after header if the response had a matching status - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - retry_config = ctx.caller_info.retry_configuration - if ctx.response is None or ( - ctx.response.status_code not in retry_config.retry_after_status_codes - ): - return RetryCheckResult.no_decision - retry_after = self.parse_retry_after(ctx.response) - if retry_after: - ctx.backoff = float(retry_after) + # run the authorizer's handler, and 'do_retry' if the handler indicated + # that it was able to make a change which should make the request retryable + if ctx.caller_info.authorizer.handle_missing_authorization(): return RetryCheckResult.do_retry + return RetryCheckResult.no_decision - def check_transient_error(self, ctx: RetryContext) -> RetryCheckResult: - """ - Check for transient error status codes which could be resolved by retrying - the request - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - retry_config = ctx.caller_info.retry_configuration - if ctx.response is not None and ( - ctx.response.status_code in retry_config.transient_error_status_codes - ): - return RetryCheckResult.do_retry - return RetryCheckResult.no_decision - @set_retry_check_flags(RetryCheckFlags.RUN_ONCE) - def check_expired_authorization(self, ctx: RetryContext) -> RetryCheckResult: - """ - This check evaluates whether or not there is invalid or expired authorization - information which could be updated with some action -- most typically a token - refresh for an expired access token. - - The check is flagged to only run once per request. - - :param ctx: The context object which describes the state of the request and the - retries which may already have been attempted. - """ - retry_config = ctx.caller_info.retry_configuration - if ( # is the current check applicable? - ctx.response is None - or ctx.caller_info is None - or ctx.caller_info.authorizer is None - or ( - ctx.response.status_code - not in retry_config.expired_authorization_status_codes - ) - ): - return RetryCheckResult.no_decision - - # run the authorizer's handler, and 'do_retry' if the handler indicated - # that it was able to make a change which should make the request retryable - if ctx.caller_info.authorizer.handle_missing_authorization(): - return RetryCheckResult.do_retry - return RetryCheckResult.no_decision +def _parse_retry_after(response: requests.Response) -> int | None: + """ + Get the 'Retry-After' header as an int. + + :param response: The response to parse. + """ + val = response.headers.get("Retry-After") + if not val: + return None + try: + return int(val) + except ValueError: + return None - def parse_retry_after(self, response: requests.Response) -> int | None: - """ - Get the 'Retry-After' header as an int. - - :param response: The response to parse. - """ - val = response.headers.get("Retry-After") - if not val: - return None - try: - return int(val) - except ValueError: - return None + +DEFAULT_RETRY_CHECKS: tuple[RetryCheck, ...] = ( + check_expired_authorization, + check_request_exception, + check_retry_after_header, + check_transient_error, +) diff --git a/src/globus_sdk/transport/requests.py b/src/globus_sdk/transport/requests.py index 8cf5dbb9f..986bf9b3d 100644 --- a/src/globus_sdk/transport/requests.py +++ b/src/globus_sdk/transport/requests.py @@ -20,7 +20,7 @@ from .caller_info import RequestCallerInfo from .retry import RetryContext from .retry_check_runner import RetryCheckRunner -from .retry_config import RetryConfiguration +from .retry_config import RetryConfig log = logging.getLogger(__name__) @@ -143,7 +143,7 @@ def tune( >>> with client.transport.tune(http_timeout=120): >>> foo = client.get_foo() - See also: :meth:`RetryConfiguration.tune`. + See also: :meth:`RetryConfig.tune`. """ saved_settings = ( self.verify_ssl, @@ -199,9 +199,7 @@ def _set_authz_header( else: req.headers.pop("Authorization", None) # remove any possible value - def _retry_sleep( - self, retry_configuration: RetryConfiguration, ctx: RetryContext - ) -> None: + def _retry_sleep(self, retry_config: RetryConfig, ctx: RetryContext) -> None: """ Given a retry context, compute the amount of time to sleep and sleep that much This is always the minimum of the backoff (run on the context) and the @@ -210,13 +208,11 @@ def _retry_sleep( :param ctx: The context object which describes the state of the request and the retries which may already have been attempted. """ - sleep_period = min( - retry_configuration.backoff(ctx), retry_configuration.max_sleep - ) + sleep_period = min(retry_config.backoff(ctx), retry_config.max_sleep) log.debug( "request retry_sleep(%s) [max=%s]", sleep_period, - retry_configuration.max_sleep, + retry_config.max_sleep, ) time.sleep(sleep_period) @@ -257,11 +253,11 @@ def request( log.debug("starting request for %s", url) resp: requests.Response | None = None req = self._encode(method, url, query_params, data, headers, encoding) - retry_configuration = caller_info.retry_configuration - checker = RetryCheckRunner(retry_configuration.checks) + retry_config = caller_info.retry_config + checker = RetryCheckRunner(caller_info.retry_config.checks) log.debug("transport request state initialized") - for attempt in range(retry_configuration.max_retries + 1): + for attempt in range(retry_config.max_retries + 1): log.debug("transport request retry cycle. attempt=%d", attempt) # add Authorization header, or (if it's a NullAuthorizer) possibly # explicitly remove the Authorization header @@ -281,10 +277,7 @@ def request( except requests.RequestException as err: log.debug("request hit error (RequestException)") ctx.exception = err - if ( - attempt >= retry_configuration.max_retries - or not checker.should_retry(ctx) - ): + if attempt >= retry_config.max_retries or not checker.should_retry(ctx): log.warning("request done (fail, error)") raise exc.convert_request_exception(err) log.debug("request may retry (should-retry=true)") @@ -296,9 +289,9 @@ def request( log.debug("request may retry, will check attempts") # the request will be retried, so sleep... - if attempt < retry_configuration.max_retries: + if attempt < retry_config.max_retries: log.debug("under attempt limit, will sleep") - self._retry_sleep(retry_configuration, ctx) + self._retry_sleep(retry_config, ctx) if resp is None: raise ValueError("Somehow, retries ended without a response") log.warning("request reached max retries, done (fail, response)") diff --git a/src/globus_sdk/transport/retry.py b/src/globus_sdk/transport/retry.py index 227d33233..0c30158fa 100644 --- a/src/globus_sdk/transport/retry.py +++ b/src/globus_sdk/transport/retry.py @@ -96,6 +96,8 @@ class RetryCheckCollection: A RetryCheckCollection is an ordered collection of retry checks which are used to determine whether or not a request should be retried. + Checks are stored in registration order. + Notably, the collection does not decide - how many times a request should retry - how or how long the call should wait between attempts @@ -107,7 +109,7 @@ class RetryCheckCollection: """ def __init__(self) -> None: - self.checks: list[RetryCheck] = [] + self._data: list[RetryCheck] = [] def register_check(self, func: RetryCheck) -> RetryCheck: """ @@ -121,8 +123,20 @@ def register_check(self, func: RetryCheck) -> RetryCheck: :param func: The function or other callable to register as a retry check """ - self.checks.append(func) + self._data.append(func) return func + def register_many_checks(self, funcs: t.Iterable[RetryCheck]) -> None: + """ + Register all checks in a collection of checks. + + :param funcs: An iterable collection of retry check callables + """ + for f in funcs: + self.register_check(f) + def __iter__(self) -> t.Iterator[RetryCheck]: - yield from self.checks + yield from self._data + + def __len__(self) -> int: + return len(self._data) diff --git a/src/globus_sdk/transport/retry_config.py b/src/globus_sdk/transport/retry_config.py index 3f3398d96..ff458865b 100644 --- a/src/globus_sdk/transport/retry_config.py +++ b/src/globus_sdk/transport/retry_config.py @@ -17,7 +17,7 @@ def _exponential_backoff(ctx: RetryContext) -> float: @dataclasses.dataclass -class RetryConfiguration: +class RetryConfig: """ Configuration for a client which is going to retry requests. @@ -25,7 +25,7 @@ class RetryConfiguration: :param max_sleep: The maximum sleep time between retries (in seconds). If the computed sleep time or the backoff requested by a retry check exceeds this value, this amount of time will be used instead. - :param retry_backoff: A function which determines how long to sleep between calls + :param backoff: A function which determines how long to sleep between calls based on the RetryContext. Defaults to exponential backoff with jitter based on the context ``attempt`` number. :param retry_after_status_codes: HTTP status codes for responses which may have @@ -38,8 +38,6 @@ class RetryConfiguration: responses and exceptions, as a ``RetryCheckCollection``. """ - checks: RetryCheckCollection - max_retries: int = 5 max_sleep: float | int = 10 backoff: t.Callable[[RetryContext], float] = _exponential_backoff @@ -47,6 +45,10 @@ class RetryConfiguration: transient_error_status_codes: tuple[int, ...] = (429, 500, 502, 503, 504) expired_authorization_status_codes: tuple[int, ...] = (401,) + checks: RetryCheckCollection = dataclasses.field( + default_factory=RetryCheckCollection + ) + @contextlib.contextmanager def tune( self, diff --git a/tests/functional/base_client/conftest.py b/tests/functional/base_client/conftest.py index d02df505d..f1204f3ac 100644 --- a/tests/functional/base_client/conftest.py +++ b/tests/functional/base_client/conftest.py @@ -4,8 +4,13 @@ @pytest.fixture -def client(): +def client_class(): class CustomClient(globus_sdk.BaseClient): service_name = "foo" - return CustomClient() + return CustomClient + + +@pytest.fixture +def client(client_class): + return client_class() diff --git a/tests/functional/base_client/test_retry_behavior.py b/tests/functional/base_client/test_retry_behavior.py index f9758ba0b..2839fbeb9 100644 --- a/tests/functional/base_client/test_retry_behavior.py +++ b/tests/functional/base_client/test_retry_behavior.py @@ -2,20 +2,16 @@ import requests import globus_sdk -from globus_sdk.testing import RegisteredResponse, load_response -from globus_sdk.transport import RequestCallerInfo +from globus_sdk.testing import RegisteredResponse +from globus_sdk.transport import RequestCallerInfo, RetryConfig @pytest.mark.parametrize("error_status", [500, 429, 502, 503, 504]) def test_retry_on_transient_error(client, mocksleep, error_status): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=error_status, body="Uh-oh!" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=error_status, body="Uh-oh!" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() # no sign of an error in the client res = client.get("/bar") @@ -27,37 +23,62 @@ def test_retry_on_transient_error(client, mocksleep, error_status): def test_retry_disabled_via_tune(client, mocksleep): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() # the error is seen by the client (automatic retry does not hide it) with pytest.raises(globus_sdk.GlobusAPIError) as excinfo: - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): client.get("/bar") assert excinfo.value.http_status == 500 - # there was a no sleep (retry was not triggered) + # there was no sleep (retry was not triggered) mocksleep.assert_not_called() +def test_retry_disabled_via_init_param(client_class, mocksleep): + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() + client = client_class(retry_config=RetryConfig(max_retries=0)) + + # the error is seen by the client (automatic retry does not hide it) + with pytest.raises(globus_sdk.GlobusAPIError) as excinfo: + client.get("/bar") + assert excinfo.value.http_status == 500 + + # there was no sleep (retry was not triggered) + mocksleep.assert_not_called() + + +def test_retry_disabled_via_init_param_but_enabled_via_tune(client_class, mocksleep): + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() + client = client_class(retry_config=RetryConfig(max_retries=0)) + + # no sign of an error in the client if we "turn it back on" + with client.retry_config.tune(max_retries=1): + res = client.get("/bar") + assert res.http_status == 200 + assert res["baz"] == 1 + + # there was a sleep (retry was triggered) + mocksleep.assert_called_once() + + def test_retry_on_network_error(client, mocksleep): # set the response to be a requests NetworkError -- responses will raise the # exception when the call is made - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", - body=requests.ConnectionError("foo-err"), - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", + body=requests.ConnectionError("foo-err"), + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() # no sign of an error in the client res = client.get("/bar") @@ -72,14 +93,10 @@ def test_retry_on_network_error(client, mocksleep): def test_retry_limit(client, mocksleep, num_errors, expect_err): # N errors followed by a success for _i in range(num_errors): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() if expect_err: with pytest.raises(globus_sdk.GlobusAPIError): @@ -96,17 +113,13 @@ def test_retry_limit(client, mocksleep, num_errors, expect_err): def test_transport_retry_limit(client, mocksleep): # this limit is a safety to protect against a bad policy causing infinite retries - client.retry_configuration.max_retries = 2 + client.retry_config.max_retries = 2 for _i in range(3): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=500, body="Uh-oh!" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() with pytest.raises(globus_sdk.GlobusAPIError): client.get("/bar") @@ -119,7 +132,7 @@ def test_bad_max_retries_causes_error(client): # to ensure that no requests are ever sent # the transport should throw an error in this case, since it doesn't have a response # value to return - client.retry_configuration.max_retries = -1 + client.retry_config.max_retries = -1 with pytest.raises(ValueError): client.get("/bar") @@ -127,29 +140,21 @@ def test_bad_max_retries_causes_error(client): def test_persistent_connection_error(client): for _i in range(6): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", - body=requests.ConnectionError("foo-err"), - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", + body=requests.ConnectionError("foo-err"), + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() with pytest.raises(globus_sdk.GlobusConnectionError): client.get("/bar") def test_no_retry_401_no_authorizer(client): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() # error gets raised in client (no retry) with pytest.raises(globus_sdk.GlobusAPIError) as excinfo: @@ -158,14 +163,10 @@ def test_no_retry_401_no_authorizer(client): def test_retry_with_authorizer(client): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() # an authorizer class which does nothing but claims to support handling of # unauthorized errors @@ -194,14 +195,10 @@ def handle_missing_authorization(self): def test_no_retry_with_authorizer_no_handler(client): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() # an authorizer class which does nothing and does not claim to handle # unauthorized errors @@ -229,19 +226,13 @@ def handle_missing_authorization(self): def test_retry_with_authorizer_persistent_401(client): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" - ) - ) - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" + ).add() + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() # an authorizer class which does nothing but claims to support handling of # unauthorized errors @@ -272,14 +263,10 @@ def handle_missing_authorization(self): def test_transport_caller_info_with_retry(client): - load_response( - RegisteredResponse( - path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" - ) - ) - load_response( - RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}) - ) + RegisteredResponse( + path="https://foo.api.globus.org/bar", status=401, body="Unauthorized" + ).add() + RegisteredResponse(path="https://foo.api.globus.org/bar", json={"baz": 1}).add() dummy_authz_calls = [] @@ -294,7 +281,7 @@ def handle_missing_authorization(self): authorizer = DummyAuthorizer() caller_info = RequestCallerInfo( - retry_configuration=client.retry_configuration, authorizer=authorizer + retry_config=client.retry_config, authorizer=authorizer ) # Test direct transport usage with caller_info diff --git a/tests/functional/services/auth/confidential_client/conftest.py b/tests/functional/services/auth/confidential_client/conftest.py index a3cc1bdf6..d680b49d6 100644 --- a/tests/functional/services/auth/confidential_client/conftest.py +++ b/tests/functional/services/auth/confidential_client/conftest.py @@ -8,5 +8,5 @@ def auth_client(): client = globus_sdk.ConfidentialAppAuthClient( "dummy_client_id", "dummy_client_secret" ) - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/auth/conftest.py b/tests/functional/services/auth/conftest.py index a1ab53523..4d72616c0 100644 --- a/tests/functional/services/auth/conftest.py +++ b/tests/functional/services/auth/conftest.py @@ -6,12 +6,12 @@ @pytest.fixture def login_client(): client = globus_sdk.AuthLoginClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client @pytest.fixture def service_client(): client = globus_sdk.AuthClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/auth/native_client/conftest.py b/tests/functional/services/auth/native_client/conftest.py index 0182d0f81..5786aef29 100644 --- a/tests/functional/services/auth/native_client/conftest.py +++ b/tests/functional/services/auth/native_client/conftest.py @@ -6,5 +6,5 @@ @pytest.fixture def auth_client(): client = globus_sdk.NativeAppAuthClient("dummy_client_id") - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/auth/test_auth_client_flow.py b/tests/functional/services/auth/test_auth_client_flow.py index 50725ca87..75ff8695e 100644 --- a/tests/functional/services/auth/test_auth_client_flow.py +++ b/tests/functional/services/auth/test_auth_client_flow.py @@ -15,7 +15,7 @@ @pytest.fixture def native_client(): client = globus_sdk.NativeAppAuthClient(client_id=CLIENT_ID) - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client @@ -24,7 +24,7 @@ def confidential_client(): client = globus_sdk.ConfidentialAppAuthClient( client_id=CLIENT_ID, client_secret="SECRET_SECRET_HES_GOT_A_SECRET" ) - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/compute/conftest.py b/tests/functional/services/compute/conftest.py index f72944d17..563cd27af 100644 --- a/tests/functional/services/compute/conftest.py +++ b/tests/functional/services/compute/conftest.py @@ -6,12 +6,12 @@ @pytest.fixture def compute_client_v2(): client = globus_sdk.ComputeClientV2() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client @pytest.fixture def compute_client_v3(): client = globus_sdk.ComputeClientV3() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/flows/conftest.py b/tests/functional/services/flows/conftest.py index 91eaf5307..ed321520a 100644 --- a/tests/functional/services/flows/conftest.py +++ b/tests/functional/services/flows/conftest.py @@ -8,7 +8,7 @@ @pytest.fixture def flows_client(): client = globus_sdk.FlowsClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client @@ -17,6 +17,6 @@ def specific_flow_client_class() -> t.Type[globus_sdk.SpecificFlowClient]: class CustomSpecificFlowClient(globus_sdk.SpecificFlowClient): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - self.retry_configuration.max_retries = 0 + self.retry_config.max_retries = 0 return CustomSpecificFlowClient diff --git a/tests/functional/services/gcs/conftest.py b/tests/functional/services/gcs/conftest.py index c4900790f..3533a6cb0 100644 --- a/tests/functional/services/gcs/conftest.py +++ b/tests/functional/services/gcs/conftest.py @@ -7,5 +7,5 @@ def client(): # default fqdn for GCS client testing client = globus_sdk.GCSClient("abc.xyz.data.globus.org") - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/groups/conftest.py b/tests/functional/services/groups/conftest.py index 7025ea1e9..76b5d9459 100644 --- a/tests/functional/services/groups/conftest.py +++ b/tests/functional/services/groups/conftest.py @@ -6,7 +6,7 @@ @pytest.fixture def groups_client(): client = globus_sdk.GroupsClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/search/conftest.py b/tests/functional/services/search/conftest.py index 4ce9f9674..82c2aa1e3 100644 --- a/tests/functional/services/search/conftest.py +++ b/tests/functional/services/search/conftest.py @@ -6,5 +6,5 @@ @pytest.fixture def client(): client = globus_sdk.SearchClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/search/test_search.py b/tests/functional/services/search/test_search.py index 7d5d2542d..442bffd63 100644 --- a/tests/functional/services/search/test_search.py +++ b/tests/functional/services/search/test_search.py @@ -14,7 +14,7 @@ @pytest.fixture def search_client(): client = globus_sdk.SearchClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/search/test_search_roles.py b/tests/functional/services/search/test_search_roles.py index e8ef9c4ea..afd77b930 100644 --- a/tests/functional/services/search/test_search_roles.py +++ b/tests/functional/services/search/test_search_roles.py @@ -9,7 +9,7 @@ @pytest.fixture def search_client(): client = globus_sdk.SearchClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/transfer/conftest.py b/tests/functional/services/transfer/conftest.py index 85b5935c8..a8397648d 100644 --- a/tests/functional/services/transfer/conftest.py +++ b/tests/functional/services/transfer/conftest.py @@ -6,5 +6,5 @@ @pytest.fixture def client(): client = globus_sdk.TransferClient() - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/functional/services/transfer/test_custom_retry_behavior.py b/tests/functional/services/transfer/test_custom_retry_behavior.py new file mode 100644 index 000000000..344d3b5f7 --- /dev/null +++ b/tests/functional/services/transfer/test_custom_retry_behavior.py @@ -0,0 +1,49 @@ +import pytest + +import globus_sdk +from globus_sdk.testing import RegisteredResponse + + +def test_transfer_client_will_retry_ordinary_502(client, mocksleep): + # turn on retries (fixture defaults off) + client.retry_config.max_retries = 1 + + RegisteredResponse(service="transfer", path="/foo", status=502, body="Uh-oh!").add() + RegisteredResponse(service="transfer", path="/foo", json={"status": "ok"}).add() + + # no sign of an error in the client + res = client.get("/foo") + assert res.http_status == 200 + assert res["status"] == "ok" + + # there was a sleep (retry was triggered) + mocksleep.assert_called_once() + + +def test_transfer_client_will_not_retry_endpoint_error(client, mocksleep): + # turn on retries (fixture defaults off) + client.retry_config.max_retries = 1 + + RegisteredResponse( + service="transfer", + path="/do_a_gcp_thing", + status=502, + json={ + "HTTP status": "502", + "code": "ExternalError.DirListingFailed.GCDisconnected", + "error_name": "Transfer API Error", + "message": "The GCP endpoint is not currently connected to Globus", + "request_id": "rhvcR0aHX", + }, + ).add() + RegisteredResponse( + service="transfer", path="/do_a_gcp_thing", json={"status": "ok"} + ).add() + + # no sign of an error in the client + with pytest.raises(globus_sdk.TransferAPIError) as excinfo: + client.get("/do_a_gcp_thing") + assert excinfo.value.http_status == 502 + + # there was no sleep (retry was not triggered) + mocksleep.assert_not_called() diff --git a/tests/functional/tokenstorage/v2/conftest.py b/tests/functional/tokenstorage/v2/conftest.py index 0ba54a457..bc6c37620 100644 --- a/tests/functional/tokenstorage/v2/conftest.py +++ b/tests/functional/tokenstorage/v2/conftest.py @@ -17,7 +17,7 @@ def id_token_sub(): @pytest.fixture def cc_auth_client(): client = globus_sdk.ConfidentialAppAuthClient("dummy_id", "dummy_secret") - with client.retry_configuration.tune(max_retries=0): + with client.retry_config.tune(max_retries=0): yield client diff --git a/tests/unit/sphinxext/test_copyparams_directive.py b/tests/unit/sphinxext/test_copyparams_directive.py index 8dedb80ce..01c058d76 100644 --- a/tests/unit/sphinxext/test_copyparams_directive.py +++ b/tests/unit/sphinxext/test_copyparams_directive.py @@ -12,6 +12,7 @@ "app_name", "base_url", "transport", + "retry_config", ) diff --git a/tests/unit/test_base_client.py b/tests/unit/test_base_client.py index 95ce97251..bf5b6da6d 100644 --- a/tests/unit/test_base_client.py +++ b/tests/unit/test_base_client.py @@ -28,7 +28,7 @@ class CustomClient(globus_sdk.BaseClient): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - self.retry_configuration.max_retries = 0 + self.retry_config.max_retries = 0 return CustomClient diff --git a/tests/unit/transport/test_default_retry_policy.py b/tests/unit/transport/test_default_retry_policy.py index 41b7c974a..fd533300e 100644 --- a/tests/unit/transport/test_default_retry_policy.py +++ b/tests/unit/transport/test_default_retry_policy.py @@ -3,26 +3,31 @@ import pytest from globus_sdk.transport import ( - DefaultRetryCheckCollection, RequestCallerInfo, RequestsTransport, RetryCheckResult, RetryCheckRunner, - RetryConfiguration, + RetryConfig, RetryContext, ) +from globus_sdk.transport.default_retry_checks import ( + DEFAULT_RETRY_CHECKS, + check_retry_after_header, + check_transient_error, +) @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_respects_retry_after(mocksleep, http_status): - retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) + retry_config = RetryConfig() + retry_config.checks.register_many_checks(DEFAULT_RETRY_CHECKS) transport = RequestsTransport() checker = RetryCheckRunner(retry_config.checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "5"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(retry_configuration=retry_config) + caller_info = RequestCallerInfo(retry_config=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True @@ -34,14 +39,15 @@ def test_retry_policy_respects_retry_after(mocksleep, http_status): @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_ignores_retry_after_too_high(mocksleep, http_status): # set explicit max sleep to confirm that the value is capped here - retry_config = RetryConfiguration(max_sleep=5, checks=DefaultRetryCheckCollection()) + retry_config = RetryConfig(max_sleep=5) + retry_config.checks.register_many_checks(DEFAULT_RETRY_CHECKS) transport = RequestsTransport() checker = RetryCheckRunner(retry_config.checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "20"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(retry_configuration=retry_config) + caller_info = RequestCallerInfo(retry_config=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True @@ -52,14 +58,15 @@ def test_retry_policy_ignores_retry_after_too_high(mocksleep, http_status): @pytest.mark.parametrize("http_status", (429, 503)) def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): - retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) + retry_config = RetryConfig() + retry_config.checks.register_many_checks(DEFAULT_RETRY_CHECKS) transport = RequestsTransport() checker = RetryCheckRunner(retry_config.checks) dummy_response = mock.Mock() dummy_response.headers = {"Retry-After": "not-an-integer"} dummy_response.status_code = http_status - caller_info = RequestCallerInfo(retry_configuration=retry_config) + caller_info = RequestCallerInfo(retry_config=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True @@ -69,15 +76,13 @@ def test_retry_policy_ignores_malformed_retry_after(mocksleep, http_status): @pytest.mark.parametrize( - "checkname", - [ - "check_retry_after_header", - "check_transient_error", - ], + "check_method", + [check_retry_after_header, check_transient_error], + ids=lambda f: f.__name__, ) -def test_default_retry_check_noop_on_exception(checkname, mocksleep): - retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) - method = getattr(retry_config.checks, checkname) - caller_info = RequestCallerInfo(retry_configuration=retry_config) +def test_default_retry_check_noop_on_exception(check_method, mocksleep): + retry_config = RetryConfig() + retry_config.checks.register_many_checks(DEFAULT_RETRY_CHECKS) + caller_info = RequestCallerInfo(retry_config=retry_config) ctx = RetryContext(1, caller_info=caller_info, exception=Exception("foo")) - assert method(ctx) is RetryCheckResult.no_decision + assert check_method(ctx) is RetryCheckResult.no_decision diff --git a/tests/unit/transport/test_retry_check_runner.py b/tests/unit/transport/test_retry_check_runner.py index e70283fa9..c4105d456 100644 --- a/tests/unit/transport/test_retry_check_runner.py +++ b/tests/unit/transport/test_retry_check_runner.py @@ -1,18 +1,19 @@ from unittest import mock from globus_sdk.transport import ( - DefaultRetryCheckCollection, RequestCallerInfo, RetryCheckResult, RetryCheckRunner, - RetryConfiguration, + RetryConfig, RetryContext, ) +from globus_sdk.transport.default_retry_checks import DEFAULT_RETRY_CHECKS def _make_test_retry_context(*, status=200, exception=None, response=None): - retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) - caller_info = RequestCallerInfo(retry_configuration=retry_config) + retry_config = RetryConfig() + retry_config.checks.register_many_checks(DEFAULT_RETRY_CHECKS) + caller_info = RequestCallerInfo(retry_config=retry_config) if exception: return RetryContext(1, caller_info=caller_info, exception=exception) elif response: diff --git a/tests/unit/transport/test_transfer_transport.py b/tests/unit/transport/test_transfer_transport.py index 44260c64a..cee273260 100644 --- a/tests/unit/transport/test_transfer_transport.py +++ b/tests/unit/transport/test_transfer_transport.py @@ -1,16 +1,35 @@ from unittest import mock -from globus_sdk.services.transfer.transport import TransferDefaultRetryCheckCollection +from globus_sdk.services.transfer.transport import TRANSFER_DEFAULT_RETRY_CHECKS from globus_sdk.transport import ( RequestCallerInfo, + RetryCheckCollection, RetryCheckRunner, - RetryConfiguration, + RetryConfig, RetryContext, ) +from globus_sdk.transport.default_retry_checks import DEFAULT_RETRY_CHECKS + + +def test_transfer_only_replaces_checks(): + # their length matches, meaning things line up + assert len(TRANSFER_DEFAULT_RETRY_CHECKS) == len(DEFAULT_RETRY_CHECKS) + + # also confirm that this holds once loaded + # if the implementation of the RetryCheckCollection becomes sensitive to + # the contents of these tuples, this could fail + default_variant = RetryCheckCollection() + default_variant.register_many_checks(DEFAULT_RETRY_CHECKS) + + transfer_variant = RetryCheckCollection() + transfer_variant.register_many_checks(TRANSFER_DEFAULT_RETRY_CHECKS) + + assert len(default_variant) == len(transfer_variant) def test_transfer_does_not_retry_external(): - retry_config = RetryConfiguration(checks=TransferDefaultRetryCheckCollection()) + retry_config = RetryConfig() + retry_config.checks.register_many_checks(TRANSFER_DEFAULT_RETRY_CHECKS) checker = RetryCheckRunner(retry_config.checks) body = { @@ -24,14 +43,15 @@ def test_transfer_does_not_retry_external(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - caller_info = RequestCallerInfo(retry_configuration=retry_config) + caller_info = RequestCallerInfo(retry_config=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False def test_transfer_does_not_retry_endpoint_error(): - retry_config = RetryConfiguration(checks=TransferDefaultRetryCheckCollection()) + retry_config = RetryConfig() + retry_config.checks.register_many_checks(TRANSFER_DEFAULT_RETRY_CHECKS) checker = RetryCheckRunner(retry_config.checks) body = { @@ -48,14 +68,15 @@ def test_transfer_does_not_retry_endpoint_error(): dummy_response = mock.Mock() dummy_response.json = lambda: body dummy_response.status_code = 502 - caller_info = RequestCallerInfo(retry_configuration=retry_config) + caller_info = RequestCallerInfo(retry_config=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is False def test_transfer_retries_others(): - retry_config = RetryConfiguration(checks=TransferDefaultRetryCheckCollection()) + retry_config = RetryConfig() + retry_config.checks.register_many_checks(TRANSFER_DEFAULT_RETRY_CHECKS) checker = RetryCheckRunner(retry_config.checks) def _raise_value_error(): @@ -64,7 +85,7 @@ def _raise_value_error(): dummy_response = mock.Mock() dummy_response.json = _raise_value_error dummy_response.status_code = 502 - caller_info = RequestCallerInfo(retry_configuration=retry_config) + caller_info = RequestCallerInfo(retry_config=retry_config) ctx = RetryContext(1, caller_info=caller_info, response=dummy_response) assert checker.should_retry(ctx) is True diff --git a/tests/unit/transport/test_transport.py b/tests/unit/transport/test_transport.py index ff888328f..3a86ec83f 100644 --- a/tests/unit/transport/test_transport.py +++ b/tests/unit/transport/test_transport.py @@ -3,12 +3,7 @@ import pytest -from globus_sdk.transport import ( - DefaultRetryCheckCollection, - RequestsTransport, - RetryConfiguration, - RetryContext, -) +from globus_sdk.transport import RequestsTransport, RetryConfig, RetryContext from globus_sdk.transport.retry_config import _exponential_backoff @@ -65,7 +60,7 @@ def test_transport_tuning(param_name, init_value, tune_value): ) def test_retry_tuning(param_name, init_value, tune_value): init_kwargs = {param_name: init_value} - config = RetryConfiguration(DefaultRetryCheckCollection(), **init_kwargs) + config = RetryConfig(**init_kwargs) assert getattr(config, param_name) == init_value diff --git a/tests/unit/transport/test_transport_authz_handling.py b/tests/unit/transport/test_transport_authz_handling.py index 458cdf203..7f4e506fe 100644 --- a/tests/unit/transport/test_transport_authz_handling.py +++ b/tests/unit/transport/test_transport_authz_handling.py @@ -3,12 +3,7 @@ import pytest from globus_sdk.authorizers import NullAuthorizer -from globus_sdk.transport import ( - DefaultRetryCheckCollection, - RequestCallerInfo, - RequestsTransport, - RetryConfiguration, -) +from globus_sdk.transport import RequestCallerInfo, RequestsTransport, RetryConfig def test_will_not_modify_authz_header_without_authorizer(): @@ -38,12 +33,12 @@ def test_will_null_authz_header_with_null_authorizer(): def test_requests_transport_accepts_caller_info(): - retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) + retry_config = RetryConfig() transport = RequestsTransport() mock_authorizer = mock.Mock() mock_authorizer.get_authorization_header.return_value = "Bearer token" caller_info = RequestCallerInfo( - retry_configuration=retry_config, authorizer=mock_authorizer + retry_config=retry_config, authorizer=mock_authorizer ) with mock.patch.object(transport, "session") as mock_session: @@ -68,9 +63,9 @@ def test_requests_transport_caller_info_required(): def test_requests_transport_keyword_only(): - retry_config = RetryConfiguration(checks=DefaultRetryCheckCollection()) + retry_config = RetryConfig() transport = RequestsTransport() - caller_info = RequestCallerInfo(retry_configuration=retry_config) + caller_info = RequestCallerInfo(retry_config=retry_config) with pytest.raises(TypeError): transport.request("GET", "https://example.com", caller_info) From fbb98972ffd01710a2a3196161c7f852229a1c6c Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 29 Jul 2025 12:16:51 -0500 Subject: [PATCH 157/176] Fix minor documentation errors Co-authored-by: Ada <107940310+ada-globus@users.noreply.github.com> --- docs/upgrading.rst | 2 +- src/globus_sdk/transport/retry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 4cf493811..8fd8a3c25 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -516,7 +516,7 @@ This could then be configured on a custom client class: client = MyClientClass() Under SDK v4, in order to customize the same information, users can simply -client and then modify the attributes of the ``retry_config`` object: +create a client and then modify the attributes of the ``retry_config`` object: .. code-block:: python diff --git a/src/globus_sdk/transport/retry.py b/src/globus_sdk/transport/retry.py index 0c30158fa..08d46e70d 100644 --- a/src/globus_sdk/transport/retry.py +++ b/src/globus_sdk/transport/retry.py @@ -104,7 +104,7 @@ class RetryCheckCollection: (except via the backoff which may be set) - what kinds of request parameters (e.g., timeouts) are used - It *only* contains ``RetryCheck`` methods which can look at a response or + It *only* contains ``RetryCheck`` functions which can look at a response or error and decide whether or not to retry. """ From d87bae9db464431262a76721ceb8a53d0948089e Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Jul 2025 22:26:15 +0000 Subject: [PATCH 158/176] (actions) update PR references --- .../20250721_202834_sirosen_change_transport_passing_style.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst b/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst index adb36bb0f..c5df11129 100644 --- a/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst +++ b/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst @@ -5,7 +5,7 @@ Breaking Changes 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`) + (:pr:`1275`) - The ``transport_class`` attribute has been removed from client classes. From d7287681f5ec052e36837a8301835cbc7373b900 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Jul 2025 22:26:50 +0000 Subject: [PATCH 159/176] (actions) update PR references --- changelog.d/20250729_101626_sirosen_remove_create_entry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250729_101626_sirosen_remove_create_entry.rst b/changelog.d/20250729_101626_sirosen_remove_create_entry.rst index 0056946e0..954f0483b 100644 --- a/changelog.d/20250729_101626_sirosen_remove_create_entry.rst +++ b/changelog.d/20250729_101626_sirosen_remove_create_entry.rst @@ -2,4 +2,4 @@ Removed ------- - Removed ``SearchClient.create_entry``. - This method was deprecated in ``globus-sdk`` version 3. (:pr:`NUMBER`) + This method was deprecated in ``globus-sdk`` version 3. (:pr:`1293`) From b22410d7326774ab13a901dc607944a3eab157fe Mon Sep 17 00:00:00 2001 From: m1yag1 <8730430+m1yag1@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:40:20 -0500 Subject: [PATCH 160/176] Update `warn_deprecated` to emit `RemovedInV5Warning` --- ...13700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst | 5 +++++ docs/core/warnings.rst | 2 +- src/globus_sdk/__init__.pyi | 4 ++-- src/globus_sdk/exc/__init__.py | 6 +++--- src/globus_sdk/exc/warnings.py | 6 +++--- 5 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst diff --git a/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst b/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst new file mode 100644 index 000000000..a4eb518f2 --- /dev/null +++ b/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst @@ -0,0 +1,5 @@ +Changed +------- + +- Update ``warn_deprecated`` to emit ``RemovedInV5Warning`` and remove + ``RemovedInV4Warning`` class (:pr:`NUMBER`) diff --git a/docs/core/warnings.rst b/docs/core/warnings.rst index 89c33e2d7..5e46c57c6 100644 --- a/docs/core/warnings.rst +++ b/docs/core/warnings.rst @@ -4,7 +4,7 @@ Warnings The following warnings can be emitted by the Globus SDK to indicate a problem, or a future change, which is not necessarily an error. -.. autoclass:: globus_sdk.RemovedInV4Warning +.. autoclass:: globus_sdk.RemovedInV5Warning :members: :show-inheritance: diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index d134245ce..e4302bf78 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -16,7 +16,7 @@ from .exc import ( GlobusSDKUsageError, GlobusTimeoutError, NetworkError, - RemovedInV4Warning, + RemovedInV5Warning, ValidationError, ) from .globus_app import ClientApp, GlobusApp, GlobusAppConfig, UserApp @@ -141,7 +141,7 @@ __all__ = ( "GlobusSDKUsageError", "GlobusTimeoutError", "NetworkError", - "RemovedInV4Warning", + "RemovedInV5Warning", "ValidationError", "ClientApp", "GlobusApp", diff --git a/src/globus_sdk/exc/__init__.py b/src/globus_sdk/exc/__init__.py index 8f0b30282..2ee4f0c3b 100644 --- a/src/globus_sdk/exc/__init__.py +++ b/src/globus_sdk/exc/__init__.py @@ -10,7 +10,7 @@ ErrorInfo, ErrorInfoContainer, ) -from .warnings import RemovedInV4Warning, warn_deprecated +from .warnings import RemovedInV5Warning, warn_deprecated __all__ = ( "GlobusError", @@ -27,14 +27,14 @@ "ErrorInfoContainer", "AuthorizationParameterInfo", "ConsentRequiredInfo", - "RemovedInV4Warning", + "RemovedInV5Warning", "warn_deprecated", ) # imports from `globus_sdk.exc.convert` are done lazily # # this ensures that we do not eagerly import `requests` when attempting to use SDK -# components which do not need it, but which do need errors (e.g., RemovedInV4Warning) +# components which do not need it, but which do need errors (e.g., RemovedInV5Warning) # and we avoid paying the performance penalty for importing the relevant dependencies if t.TYPE_CHECKING: from .convert import ( diff --git a/src/globus_sdk/exc/warnings.py b/src/globus_sdk/exc/warnings.py index 1050c8499..60b1e99c7 100644 --- a/src/globus_sdk/exc/warnings.py +++ b/src/globus_sdk/exc/warnings.py @@ -3,14 +3,14 @@ import warnings -class RemovedInV4Warning(DeprecationWarning): +class RemovedInV5Warning(DeprecationWarning): """ This warning indicates that a feature or usage was detected which will be - unsupported in globus-sdk version 4. + unsupported in globus-sdk version 5. Users are encouraged to resolve these warnings when possible. """ def warn_deprecated(message: str, stacklevel: int = 2) -> None: - warnings.warn(message, RemovedInV4Warning, stacklevel=stacklevel) + warnings.warn(message, RemovedInV5Warning, stacklevel=stacklevel) From fc09db374979baa4b2d8f0e126f7b3f9a5264c56 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 31 Jul 2025 19:31:16 +0000 Subject: [PATCH 161/176] (actions) update PR references --- ...31_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst b/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst index a4eb518f2..67d106f49 100644 --- a/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst +++ b/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst @@ -2,4 +2,4 @@ Changed ------- - Update ``warn_deprecated`` to emit ``RemovedInV5Warning`` and remove - ``RemovedInV4Warning`` class (:pr:`NUMBER`) + ``RemovedInV4Warning`` class (:pr:`1295`) From 103c9f9e7832ad8ce1eea7801fcb632467b91733 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 31 Jul 2025 15:10:11 -0500 Subject: [PATCH 162/176] Bump version and changelog for release --- ...17_154854_sirosen_bury_v1_tokenstorage.rst | 8 --- ...sirosen_change_transport_passing_style.rst | 25 ------- ...04_sirosen_cleanup_scope_normalization.rst | 12 ---- ...0729_094933_sirosen_remove_filter_role.rst | 5 -- ...729_101404_sirosen_remove_update_entry.rst | 5 -- ...729_101626_sirosen_remove_create_entry.rst | 5 -- ...3001_sirosen_remove_search_query_class.rst | 5 -- ...m1yag1_sc_43139_update_warn_deprecated.rst | 5 -- changelog.rst | 68 +++++++++++++++++++ pyproject.toml | 2 +- 10 files changed, 69 insertions(+), 71 deletions(-) delete mode 100644 changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst delete mode 100644 changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst delete mode 100644 changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst delete mode 100644 changelog.d/20250729_094933_sirosen_remove_filter_role.rst delete mode 100644 changelog.d/20250729_101404_sirosen_remove_update_entry.rst delete mode 100644 changelog.d/20250729_101626_sirosen_remove_create_entry.rst delete mode 100644 changelog.d/20250729_103001_sirosen_remove_search_query_class.rst delete mode 100644 changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst diff --git a/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst b/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst deleted file mode 100644 index 1baac6d05..000000000 --- a/changelog.d/20250717_154854_sirosen_bury_v1_tokenstorage.rst +++ /dev/null @@ -1,8 +0,0 @@ -Changed -------- - -- The legacy token storage adapters are now only available from the - ``globus_sdk.token_storage.legacy`` subpackage. - - Users are encouraged to migrate to the newer tooling available directly from - ``globus_sdk.token_storage``. (:pr:`1290`) diff --git a/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst b/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst deleted file mode 100644 index c5df11129..000000000 --- a/changelog.d/20250721_202834_sirosen_change_transport_passing_style.rst +++ /dev/null @@ -1,25 +0,0 @@ -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:`1275`) - - - 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. diff --git a/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst b/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst deleted file mode 100644 index f788fb3b4..000000000 --- a/changelog.d/20250725_172504_sirosen_cleanup_scope_normalization.rst +++ /dev/null @@ -1,12 +0,0 @@ -Breaking Changes ----------------- - -- Interfaces for normalizing scope data have changed. (:pr:`1289`) - - - The ``scopes_to_str`` function has been replaced with - ``ScopeParser.serialize``. - - - ``ScopeParser.serialize`` will raise an error if the serialized data is - empty. A flag, ``reject_empty=False``, can be passed to disable this check. - - - The ``scopes_to_scope_list`` function has been removed. diff --git a/changelog.d/20250729_094933_sirosen_remove_filter_role.rst b/changelog.d/20250729_094933_sirosen_remove_filter_role.rst deleted file mode 100644 index 8ca4e78f8..000000000 --- a/changelog.d/20250729_094933_sirosen_remove_filter_role.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -------- - -- Removed the ``filter_role`` parameter to ``FlowsClient.list_flows``. - This parameter was deprecated in ``globus-sdk`` version 3. (:pr:`1291`) diff --git a/changelog.d/20250729_101404_sirosen_remove_update_entry.rst b/changelog.d/20250729_101404_sirosen_remove_update_entry.rst deleted file mode 100644 index 85512f7a6..000000000 --- a/changelog.d/20250729_101404_sirosen_remove_update_entry.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -------- - -- Removed ``SearchClient.update_entry``. - This method was deprecated in ``globus-sdk`` version 3. (:pr:`1292`) diff --git a/changelog.d/20250729_101626_sirosen_remove_create_entry.rst b/changelog.d/20250729_101626_sirosen_remove_create_entry.rst deleted file mode 100644 index 954f0483b..000000000 --- a/changelog.d/20250729_101626_sirosen_remove_create_entry.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -------- - -- Removed ``SearchClient.create_entry``. - This method was deprecated in ``globus-sdk`` version 3. (:pr:`1293`) diff --git a/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst b/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst deleted file mode 100644 index 4f1d776ee..000000000 --- a/changelog.d/20250729_103001_sirosen_remove_search_query_class.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -------- - -- Removed the ``SearchQuery`` type. Users should use ``SearchQueryV1`` instead. - ``SearchQuery`` was deprecated in ``globus-sdk`` version 3. (:pr:`1294`) diff --git a/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst b/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst deleted file mode 100644 index 67d106f49..000000000 --- a/changelog.d/20250731_113700_8730430+m1yag1_sc_43139_update_warn_deprecated.rst +++ /dev/null @@ -1,5 +0,0 @@ -Changed -------- - -- Update ``warn_deprecated`` to emit ``RemovedInV5Warning`` and remove - ``RemovedInV4Warning`` class (:pr:`1295`) diff --git a/changelog.rst b/changelog.rst index 6014ffc9c..3a8567c06 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,74 @@ to a major new version of the SDK. .. scriv-insert-here +.. _changelog-4.0.0b1: + +v4.0.0b1 (2025-07-31) +===================== + +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:`1275`) + + - 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. + +- Interfaces for normalizing scope data have changed. (:pr:`1289`) + + - The ``scopes_to_str`` function has been replaced with + ``ScopeParser.serialize``. + + - ``ScopeParser.serialize`` will raise an error if the serialized data is + empty. A flag, ``reject_empty=False``, can be passed to disable this check. + + - The ``scopes_to_scope_list`` function has been removed. + +Removed +------- + +- Removed the ``filter_role`` parameter to ``FlowsClient.list_flows``. + This parameter was deprecated in ``globus-sdk`` version 3. (:pr:`1291`) + +- Removed ``SearchClient.update_entry``. + This method was deprecated in ``globus-sdk`` version 3. (:pr:`1292`) + +- Removed ``SearchClient.create_entry``. + This method was deprecated in ``globus-sdk`` version 3. (:pr:`1293`) + +- Removed the ``SearchQuery`` type. Users should use ``SearchQueryV1`` instead. + ``SearchQuery`` was deprecated in ``globus-sdk`` version 3. (:pr:`1294`) + +Changed +------- + +- The legacy token storage adapters are now only available from the + ``globus_sdk.token_storage.legacy`` subpackage. + + Users are encouraged to migrate to the newer tooling available directly from + ``globus_sdk.token_storage``. (:pr:`1290`) + +- Update ``warn_deprecated`` to emit ``RemovedInV5Warning`` and remove + ``RemovedInV4Warning`` class (:pr:`1295`) + .. _changelog-4.0.0a4: v4.0.0a4 (2025-07-25) diff --git a/pyproject.toml b/pyproject.toml index dd101de17..d54486fcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "globus-sdk" -version = "4.0.0a4" +version = "4.0.0b1" authors = [ { name = "Globus Team", email = "support@globus.org" }, ] From c4a2e32225ac66137bd9ce50c902ba21797cf5f0 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 5 Aug 2025 14:39:53 -0500 Subject: [PATCH 163/176] Put error text in `__notes__` where supported For Python 3.11+, `add_note()` and `__notes__` provide a mechanism for us to greatly enhance the representation of an error. --- .../20250805_143650_sirosen_exception_add_note.rst | 6 ++++++ src/globus_sdk/exc/api.py | 11 +++++++++++ tests/unit/errors/test_common_functionality.py | 13 +++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 changelog.d/20250805_143650_sirosen_exception_add_note.rst diff --git a/changelog.d/20250805_143650_sirosen_exception_add_note.rst b/changelog.d/20250805_143650_sirosen_exception_add_note.rst new file mode 100644 index 000000000..9f3c3f6d7 --- /dev/null +++ b/changelog.d/20250805_143650_sirosen_exception_add_note.rst @@ -0,0 +1,6 @@ +Added +----- + +- On Python 3.11+, the SDK will populate the ``__notes__`` of API errors with a + message containing the full body of the error response. + ``__notes__`` is part of the default presentation of a traceback. (:pr:`NUMBER`) diff --git a/src/globus_sdk/exc/api.py b/src/globus_sdk/exc/api.py index fcf29b109..55f735591 100644 --- a/src/globus_sdk/exc/api.py +++ b/src/globus_sdk/exc/api.py @@ -2,6 +2,8 @@ import enum import logging +import sys +import textwrap import typing as t from globus_sdk._internal import guards @@ -53,6 +55,15 @@ def __init__(self, r: requests.Response, *args: t.Any, **kwargs: t.Any) -> None: self._info: ErrorInfoContainer | None = None self._underlying_response = r self._parse_response() + + if sys.version_info >= (3, 11): + self.add_note( + ( + "This exception was caused by an API error. " + "The response body is as follows:\n\n" + ) + + textwrap.indent(self.text, " ") + ) super().__init__(*self._get_args()) @property diff --git a/tests/unit/errors/test_common_functionality.py b/tests/unit/errors/test_common_functionality.py index b69fa9176..8b54df656 100644 --- a/tests/unit/errors/test_common_functionality.py +++ b/tests/unit/errors/test_common_functionality.py @@ -1,4 +1,6 @@ import itertools +import sys +import uuid import pytest import requests @@ -52,6 +54,17 @@ def test_binary_content_property(): assert err.binary_content == body_text.encode("utf-8") +# `add_note()` and `__notes__` are new in Python 3.11 +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Exception.add_note() is new in Python 3.11" +) +def test_notes_are_populated_with_text(): + text_body = f"some error: {uuid.uuid4()}" + err = construct_error(body=text_body, http_status=400) + assert err.text == text_body + assert any(text_body in note for note in err.__notes__) + + @pytest.mark.parametrize( "body, response_headers, http_status, expect_code, expect_message", ( From b8aa68f811c6a2ab10f95c52d387b3b64e922e7f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 6 Aug 2025 18:57:02 +0000 Subject: [PATCH 164/176] (actions) update PR references --- changelog.d/20250805_143650_sirosen_exception_add_note.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250805_143650_sirosen_exception_add_note.rst b/changelog.d/20250805_143650_sirosen_exception_add_note.rst index 9f3c3f6d7..bae448698 100644 --- a/changelog.d/20250805_143650_sirosen_exception_add_note.rst +++ b/changelog.d/20250805_143650_sirosen_exception_add_note.rst @@ -3,4 +3,4 @@ Added - On Python 3.11+, the SDK will populate the ``__notes__`` of API errors with a message containing the full body of the error response. - ``__notes__`` is part of the default presentation of a traceback. (:pr:`NUMBER`) + ``__notes__`` is part of the default presentation of a traceback. (:pr:`1299`) From 99d5eb78c4d3879eef17e64a7645f9cac949c172 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Sat, 9 Aug 2025 12:40:42 -0500 Subject: [PATCH 165/176] Set pytest-xdist parallelism to 0 under `tox p` When tox parallel invocation is used, disable parallel execution under xdist. Having two layers of parallelism results in slower test executions because it increases contention. For example, on a 4 processor machine, running 4 jobs under `tox run-parallel`, the nested structure spawns 20 parallel processes: (4 xdist workers + 1 xdist main process) * 4 tox workers A more appropriate level of parallelism for 4 processors with CPU-bound work would be closer to 4. This configuration is already in use in globus-cli, and makes our configuration select parallelism at "the appropriate level". --- toxfile.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/toxfile.py b/toxfile.py index e4fcf746f..592ca65b4 100644 --- a/toxfile.py +++ b/toxfile.py @@ -52,6 +52,15 @@ def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: @impl def tox_before_run_commands(tox_env: ToxEnv) -> None: + # determine if it was a parallel invocation by examining the CLI command + parallel_detected = tox_env.options.command in ("p", "run-parallel") + if parallel_detected: + # tox is running parallel, set an indicator env var + # and effectively disable pytest-xdist by setting xdist-workers to 0 + # -- 0 means tests will run in the main process, not even in a worker + setenv = tox_env.conf.load("set_env") + setenv.update({"TOX_PARALLEL": "1", "PYTEST_XDIST_AUTO_NUM_WORKERS": "0"}) + sdk_rmtree = tox_env.conf.load("globus_sdk_rmtree") for name in sdk_rmtree: path = pathlib.Path(name) From 8a087eb57b7ca21d92c1032367fef554b480bd39 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Sat, 9 Aug 2025 12:12:01 -0500 Subject: [PATCH 166/176] Raise TypeError on invalid Scope constructions Type hints declare that `Scope.with_dependency` and `Scope.with_dependencies` require `Scope` objects as their input. This new requirement makes construction simpler, with fewer cases, but is easy to accidentally violate when manipulating Scopes and strings together. Some existing tests were actually constructing invalid scopes by passing strings and only incidentally passing. A Scope containing dependencies which are strings is invalid -- we offer no guarantees around how such an object might behave. To fix, TypeErrors are added to `with_dependency()` and `with_dependencies()` in order to catch string inputs. Co-authored-by: Kurt McKee --- ...20120_sirosen_scopes_strict_at_runtime.rst | 6 ++++ src/globus_sdk/scopes/representation.py | 15 +++++++- .../test_client_credentials_authorizer.py | 2 +- tests/unit/scopes/test_merge_scopes.py | 12 +++---- tests/unit/scopes/test_scope_model.py | 35 +++++++++++++++++++ 5 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst diff --git a/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst b/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst new file mode 100644 index 000000000..365789be4 --- /dev/null +++ b/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst @@ -0,0 +1,6 @@ +Changed +------- + +- Passing non-``Scope`` types to ``Scope.with_dependency`` and + ``Scope.with_dependencies`` now raises a ``TypeError``. Previously, this was + allowed at runtime but created an invalid ``Scope`` object. (:pr:`NUMBER`) diff --git a/src/globus_sdk/scopes/representation.py b/src/globus_sdk/scopes/representation.py index 15c4697d8..bb031ad82 100644 --- a/src/globus_sdk/scopes/representation.py +++ b/src/globus_sdk/scopes/representation.py @@ -77,6 +77,11 @@ def with_dependency(self, other_scope: Scope) -> Scope: :param other_scope: The scope upon which the current scope depends. """ + if not isinstance(other_scope, Scope): + raise TypeError( + "Scope.with_dependency() takes a Scope as its input. " + f"Got: '{type(other_scope).__qualname__}'" + ) return dataclasses.replace( self, dependencies=self.dependencies + (other_scope,) ) @@ -89,8 +94,16 @@ def with_dependencies(self, other_scopes: t.Iterable[Scope]) -> Scope: :param other_scopes: The scopes upon which the current scope depends. """ + other_scopes_tuple = tuple(other_scopes) + for i, item in enumerate(other_scopes_tuple): + if not isinstance(item, Scope): + raise TypeError( + "Scope.with_dependencies() takes " + "an iterable of Scopes as its input. " + f"At position {i}, got: '{type(item).__qualname__}'" + ) return dataclasses.replace( - self, dependencies=self.dependencies + tuple(other_scopes) + self, dependencies=self.dependencies + other_scopes_tuple ) def with_optional(self, optional: bool) -> Scope: diff --git a/tests/unit/authorizers/test_client_credentials_authorizer.py b/tests/unit/authorizers/test_client_credentials_authorizer.py index 1ceecb962..d2019838c 100644 --- a/tests/unit/authorizers/test_client_credentials_authorizer.py +++ b/tests/unit/authorizers/test_client_credentials_authorizer.py @@ -67,6 +67,6 @@ def test_can_create_authorizer_from_scope_objects(client): assert a1.scopes == "foo" a2 = ClientCredentialsAuthorizer( - client, [Scope("foo"), "bar", Scope("baz").with_dependency("buzz")] + client, [Scope("foo"), "bar", Scope("baz").with_dependency(Scope("buzz"))] ) assert a2.scopes == "foo bar baz[buzz]" diff --git a/tests/unit/scopes/test_merge_scopes.py b/tests/unit/scopes/test_merge_scopes.py index df4734f68..5fe2e1c23 100644 --- a/tests/unit/scopes/test_merge_scopes.py +++ b/tests/unit/scopes/test_merge_scopes.py @@ -25,8 +25,8 @@ def test_mixed_optional_dependencies(): def test_different_dependencies(): - s1 = [Scope("foo").with_dependency("bar")] - s2 = [Scope("foo").with_dependency("baz")] + s1 = [Scope("foo").with_dependency(Scope("bar"))] + s2 = [Scope("foo").with_dependency(Scope("baz"))] merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 1 assert merged[0].scope_string == "foo" @@ -38,8 +38,8 @@ def test_different_dependencies(): def test_optional_dependencies(): - s1 = [Scope("foo").with_dependency("bar")] - s2 = [Scope("foo").with_dependency("*bar")] + s1 = [Scope("foo").with_dependency(Scope("bar"))] + s2 = [Scope("foo").with_dependency(Scope("bar", optional=True))] merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 1 assert merged[0].scope_string == "foo" @@ -50,8 +50,8 @@ def test_optional_dependencies(): def test_different_dependencies_on_mixed_optional_base(): - s1 = [Scope("foo").with_dependency("bar")] - s2 = [Scope("foo", optional=True).with_dependency("baz")] + s1 = [Scope("foo").with_dependency(Scope("bar"))] + s2 = [Scope("foo", optional=True).with_dependency(Scope("baz"))] merged = ScopeParser.merge_scopes(s1, s2) assert len(merged) == 2 diff --git a/tests/unit/scopes/test_scope_model.py b/tests/unit/scopes/test_scope_model.py index 8f7e13d61..07e9c7465 100644 --- a/tests/unit/scopes/test_scope_model.py +++ b/tests/unit/scopes/test_scope_model.py @@ -1,5 +1,7 @@ import uuid +import pytest + from globus_sdk.scopes import Scope @@ -37,3 +39,36 @@ def test_scope_with_optional_leaves_original_unchanged(): assert not s1.optional assert s2.optional assert not s3.optional + + +def test_scope_with_string_dependency_gets_typeerror(): + s = Scope("x") + with pytest.raises( + TypeError, + match=r"Scope\.with_dependency\(\) takes a Scope as its input\. Got: 'str'", + ): + s.with_dependency("y") + + +def test_scope_with_string_dependencies_gets_typeerror(): + s = Scope("x") + with pytest.raises( + TypeError, + match=( + r"Scope\.with_dependencies\(\) takes an iterable of Scopes as its input\. " + "At position 0, got: 'str'" + ), + ): + s.with_dependencies(["y"]) + + +def test_scope_with_mixed_dependencies_gets_typeerror(): + s = Scope("x") + with pytest.raises( + TypeError, + match=( + r"Scope\.with_dependencies\(\) takes an iterable of Scopes as its input\. " + "At position 1, got: 'str'" + ), + ): + s.with_dependencies([Scope("y"), "z"]) From eca4f9b5c0d6d5b8f8fe742a1f157d3817bb784b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 11 Aug 2025 17:06:54 +0000 Subject: [PATCH 167/176] (actions) update PR references --- .../20250809_120120_sirosen_scopes_strict_at_runtime.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst b/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst index 365789be4..20661b1ae 100644 --- a/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst +++ b/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst @@ -3,4 +3,4 @@ Changed - Passing non-``Scope`` types to ``Scope.with_dependency`` and ``Scope.with_dependencies`` now raises a ``TypeError``. Previously, this was - allowed at runtime but created an invalid ``Scope`` object. (:pr:`NUMBER`) + allowed at runtime but created an invalid ``Scope`` object. (:pr:`1300`) From 819b79caa2bd16bba03a56bfb21dad44eab9882f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 17 Sep 2025 16:54:09 +0000 Subject: [PATCH 168/176] (actions) update PR references --- changelog.d/20250916_190714_sirosen_merge_back_main.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250916_190714_sirosen_merge_back_main.rst b/changelog.d/20250916_190714_sirosen_merge_back_main.rst index b19836bad..c987d6212 100644 --- a/changelog.d/20250916_190714_sirosen_merge_back_main.rst +++ b/changelog.d/20250916_190714_sirosen_merge_back_main.rst @@ -2,7 +2,7 @@ Removed ------- - The following methods and parameters, which were deprecated in globus-sdk v3, - have been removed (:pr:`NUMBER`): + have been removed (:pr:`1309`): - The ``skip_activation_check`` parameter for ``TransferData`` and ``DeleteData``. - The ``recursive_symlinks`` parameter for ``TransferData``. From 0d8671ef84835f0f3e15ac7f4d50ffe1ff850ca7 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 22 Sep 2025 14:07:36 -0500 Subject: [PATCH 169/176] Add guidance on 'base_path' to upgrading doc Because this only impacts users who are directly using HTTP methods, the expected impact of this change is somewhat limited. As such, it is placed at the end of the guide. In brief, the guide lists which clients and which methods of said clients are impacted, as well as giving notes on: - how the mapping of usages to URIs has changed - impact to the testing tools - compatible usage which does the same thing on both versions --- docs/upgrading.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 906296abd..62852059e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -563,6 +563,58 @@ A ``retry_config`` can also be passed to clients on initialization: client = globus_sdk.GroupsClient(retry_config=RetryConfig(max_retries=2)) my_groups = client.get_my_groups() +Clients No Longer Define ``base_path`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In version 3 and earlier, client classes defined an attribute ``base_path`` +which was joined as a prefix to request paths for the HTTP methods: ``get()``, +``put()``, ``post()``, ``patch()``, ``delete()``, ``head()``, and ``request()``. +The ``base_path`` attribute has been removed and direct use of HTTP APIs now +requires the full path when bare HTTP methods are used. + +``base_path`` values were also used in the testing tools defined by +``globus_sdk.testing`` and have similarly been removed. + +The ``base_path`` was automatically deduplicated when provided to SDK version 3, +meaning that code which includes this prefix will work on both SDK version 3 and +version 4. + +For example, ``TransferClient`` defined a ``base_path`` of ``"v0.10"``. +As a result, the request URI for a ``get()`` HTTP call would be mapped as follows: + +.. code-block:: python + + import globus_sdk + + tc = globus_sdk.TransferClient() + + # GET https://transfer.api.globus.org/v0.10/foo/bar + tc.get("/foo/bar") + +In version 4, without the ``base_path``, the mapping is as follows: + +.. code-block:: python + + # GET https://transfer.api.globus.org/foo/bar + tc.get("/foo/bar") + +Due to the deduplication of a leading ``base_path`` in version 3, the following +snippet has the same effect in both versions: + +.. code-block:: python + + # GET https://transfer.api.globus.org/v0.10/foo/bar + tc.get("/v0.10/foo/bar") + +Clients with a ``base_path`` and the values they defined in version 3 are listed +below. + +.. csv-table:: + :header: "Client Class", "base_path" + + "``TransferClient``", "``"v0.10"``" + "``GroupsClient``", "``"v2"``" + From 1.x or 2.x to 3.0 ----------------------- From 5b01dbcf6b3529c17f12e3eac6966803154fab9d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 23 Sep 2025 11:26:55 -0500 Subject: [PATCH 170/176] Convert malformed csv-table to simple table --- docs/upgrading.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 62852059e..e7c742a6e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -609,11 +609,12 @@ snippet has the same effect in both versions: Clients with a ``base_path`` and the values they defined in version 3 are listed below. -.. csv-table:: - :header: "Client Class", "base_path" - - "``TransferClient``", "``"v0.10"``" - "``GroupsClient``", "``"v2"``" +================== =========== +Client Class base_path +================== =========== +``TransferClient`` ``"v0.10"`` +``GroupsClient`` ``"v2"`` +================== =========== From 1.x or 2.x to 3.0 From 0e9d428484f598b23ee7fa2d1a970c938e8d63dc Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 23 Sep 2025 14:27:13 -0500 Subject: [PATCH 171/176] Fix doc title for SDK v4 (#1313) --- docs/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 42fec9371..5e12036bd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,7 @@ version = globus_sdk.__version__ # The full version, including alpha/beta/rc tags. release = version +major_version = version.partition(".")[0] issues_github_path = "globus/globus-sdk-python" @@ -56,7 +57,7 @@ # HTML Theme Options html_show_sourcelink = True html_theme = "furo" -html_title = "globus-sdk v3" +html_title = f"globus-sdk v{major_version}" html_theme_options = { "light_css_variables": { "color-brand-primary": "#27518F", From aa91e8349c7ebb04aa117e4bab6c6cee1934ad9e Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 24 Sep 2025 11:21:38 -0500 Subject: [PATCH 172/176] Update '_testing' usages for 4.x --- src/globus_sdk/testing/data/search/update_index.py | 2 +- tests/functional/services/search/test_update_index.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/globus_sdk/testing/data/search/update_index.py b/src/globus_sdk/testing/data/search/update_index.py index f43ccf7bf..51e4e4d28 100644 --- a/src/globus_sdk/testing/data/search/update_index.py +++ b/src/globus_sdk/testing/data/search/update_index.py @@ -1,6 +1,6 @@ import uuid -from globus_sdk._testing.models import RegisteredResponse, ResponseSet +from globus_sdk.testing.models import RegisteredResponse, ResponseSet INDEX_ID = str(uuid.uuid4()) diff --git a/tests/functional/services/search/test_update_index.py b/tests/functional/services/search/test_update_index.py index 49e1acbab..27f798af6 100644 --- a/tests/functional/services/search/test_update_index.py +++ b/tests/functional/services/search/test_update_index.py @@ -1,7 +1,7 @@ import pytest import globus_sdk -from globus_sdk._testing import load_response +from globus_sdk.testing import load_response def test_update_index(client): From ee24f9cd2c2363fb0585a50a41d729ae48a13220 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 24 Sep 2025 11:37:14 -0500 Subject: [PATCH 173/176] Bump version and changelog for release --- ...0805_143650_sirosen_exception_add_note.rst | 6 ---- ...20120_sirosen_scopes_strict_at_runtime.rst | 6 ---- ...0250916_190714_sirosen_merge_back_main.rst | 9 ------ changelog.rst | 29 +++++++++++++++++++ pyproject.toml | 2 +- 5 files changed, 30 insertions(+), 22 deletions(-) delete mode 100644 changelog.d/20250805_143650_sirosen_exception_add_note.rst delete mode 100644 changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst delete mode 100644 changelog.d/20250916_190714_sirosen_merge_back_main.rst diff --git a/changelog.d/20250805_143650_sirosen_exception_add_note.rst b/changelog.d/20250805_143650_sirosen_exception_add_note.rst deleted file mode 100644 index bae448698..000000000 --- a/changelog.d/20250805_143650_sirosen_exception_add_note.rst +++ /dev/null @@ -1,6 +0,0 @@ -Added ------ - -- On Python 3.11+, the SDK will populate the ``__notes__`` of API errors with a - message containing the full body of the error response. - ``__notes__`` is part of the default presentation of a traceback. (:pr:`1299`) diff --git a/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst b/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst deleted file mode 100644 index 20661b1ae..000000000 --- a/changelog.d/20250809_120120_sirosen_scopes_strict_at_runtime.rst +++ /dev/null @@ -1,6 +0,0 @@ -Changed -------- - -- Passing non-``Scope`` types to ``Scope.with_dependency`` and - ``Scope.with_dependencies`` now raises a ``TypeError``. Previously, this was - allowed at runtime but created an invalid ``Scope`` object. (:pr:`1300`) diff --git a/changelog.d/20250916_190714_sirosen_merge_back_main.rst b/changelog.d/20250916_190714_sirosen_merge_back_main.rst deleted file mode 100644 index c987d6212..000000000 --- a/changelog.d/20250916_190714_sirosen_merge_back_main.rst +++ /dev/null @@ -1,9 +0,0 @@ -Removed -------- - -- The following methods and parameters, which were deprecated in globus-sdk v3, - have been removed (:pr:`1309`): - - - The ``skip_activation_check`` parameter for ``TransferData`` and ``DeleteData``. - - The ``recursive_symlinks`` parameter for ``TransferData``. - - The ``add_symlink_item`` method of ``TransferData``. diff --git a/changelog.rst b/changelog.rst index e560f1e78..5972a42bf 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,35 @@ to a major new version of the SDK. .. scriv-insert-here +.. _changelog-4.0.0b2: + +v4.0.0b2 (2025-09-24) +===================== + +Added +----- + +- On Python 3.11+, the SDK will populate the ``__notes__`` of API errors with a + message containing the full body of the error response. + ``__notes__`` is part of the default presentation of a traceback. (:pr:`1299`) + +Removed +------- + +- The following methods and parameters, which were deprecated in globus-sdk v3, + have been removed (:pr:`1309`): + + - The ``skip_activation_check`` parameter for ``TransferData`` and ``DeleteData``. + - The ``recursive_symlinks`` parameter for ``TransferData``. + - The ``add_symlink_item`` method of ``TransferData``. + +Changed +------- + +- Passing non-``Scope`` types to ``Scope.with_dependency`` and + ``Scope.with_dependencies`` now raises a ``TypeError``. Previously, this was + allowed at runtime but created an invalid ``Scope`` object. (:pr:`1300`) + .. _changelog-4.0.0b1: v4.0.0b1 (2025-07-31) diff --git a/pyproject.toml b/pyproject.toml index d54486fcf..d14a8a7ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "globus-sdk" -version = "4.0.0b1" +version = "4.0.0b2" authors = [ { name = "Globus Team", email = "support@globus.org" }, ] From 741179249cc4a5c866b38fe8646c6d69f42d173b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 29 Sep 2025 19:08:56 +0000 Subject: [PATCH 174/176] (actions) update PR references --- .../20250929_092604_kurtmckee_update_auth_get_my_groups.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20250929_092604_kurtmckee_update_auth_get_my_groups.rst b/changelog.d/20250929_092604_kurtmckee_update_auth_get_my_groups.rst index 6f00a409e..c4269977d 100644 --- a/changelog.d/20250929_092604_kurtmckee_update_auth_get_my_groups.rst +++ b/changelog.d/20250929_092604_kurtmckee_update_auth_get_my_groups.rst @@ -1,4 +1,4 @@ Added ----- -- Add the ``statuses`` parameter to ``GroupsClient.get_my_groups()``. (:pr:`NUMBER`) +- Add the ``statuses`` parameter to ``GroupsClient.get_my_groups()``. (:pr:`1317`) From 8ce289cf10508cd81f47b986cf5ce6774fac46c7 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 6 Oct 2025 21:33:07 -0500 Subject: [PATCH 175/176] Add ScopeCollections to upgrading guide (#1328) --- docs/upgrading.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index e7c742a6e..532cfe347 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -181,6 +181,40 @@ And convert usage which builds scope objects like so: my_scope: Scope = AuthScopes.openid +``ScopeBuilder``\s are now ``ScopeCollection``\s +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As part of the refactor of scope constants, the objects which were previously +called "scope builders" are now "scope collections". +Scope collections may be static or dynamic, depending on whether or not they +statically provide their scopes at the class level or dynamically compute scopes +as instance attributes. + +The following entities are therefore renamed in addition to having changes to +their implementations: + +.. csv-table:: + :header: "Old name", "New name" + + "``GCSEndpointScopeBuilder``", "``GCSEndpointScopes``" + "``GCSCollectionScopeBuilder``", "``GCSCollectionScopes``" + "``SpecificFlowScopeBuilder``", "``SpecificFlowScopes``" + +Scope collections provide ``Scope`` objects, not strings. +Therefore, update code like so: + +.. code-block:: python + + # globus-sdk v3 + from globus_sdk.scopes import Scope, SpecificFlowScopeBuilder + + my_flow_scope = Scope(SpecificFlowScopeBuilder(FLOW_ID).user) + + # globus-sdk v4 + from globus_sdk.scopes import SpecificFlowScopes + + my_flow_scope = SpecificFlowScopes(FLOW_ID).user + Scopes Are Immutable and Have New Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 5b9e0175cb8250b13978e0cd5f5af7457b65377c Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 8 Oct 2025 13:28:08 -0500 Subject: [PATCH 176/176] Bump version and changelog for release --- changelog.rst | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/changelog.rst b/changelog.rst index bdbff3a35..a2334dd91 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,13 @@ to a major new version of the SDK. .. scriv-insert-here +.. _changelog-4.0.0: + +v4.0.0 (2025-10-08) +=================== + +*No changes from v4.0.0b2* + .. _changelog-4.0.0b2: v4.0.0b2 (2025-09-24) diff --git a/pyproject.toml b/pyproject.toml index d14a8a7ed..7cb673404 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "globus-sdk" -version = "4.0.0b2" +version = "4.0.0" authors = [ { name = "Globus Team", email = "support@globus.org" }, ]