From bcb46a0c49eb582ad98a02c2ff23cc19a8431e4e Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 4 Oct 2024 15:26:29 -0500 Subject: [PATCH 1/4] Implement add_app_transfer_data_access_scope This is a Timers helper for setting up a data_access scope in the correct nested context. It has identical validation to the Transfer equivalent, and is tested for parity. --- ...509_sirosen_timers_client_scope_helper.rst | 7 ++ src/globus_sdk/services/timers/client.py | 77 ++++++++++++++++++- .../globus_app/test_client_integration.py | 53 ++++++++++++- 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 changelog.d/20241004_152509_sirosen_timers_client_scope_helper.rst diff --git a/changelog.d/20241004_152509_sirosen_timers_client_scope_helper.rst b/changelog.d/20241004_152509_sirosen_timers_client_scope_helper.rst new file mode 100644 index 000000000..a8e604754 --- /dev/null +++ b/changelog.d/20241004_152509_sirosen_timers_client_scope_helper.rst @@ -0,0 +1,7 @@ +Added +~~~~~ + +- Add ``TimersClient.add_app_transfer_data_access_scope`` for ``TimersClient`` + instances which are integrated with ``GlobusApp``. This method registers the + nested scope dependency for a ``data_access`` requirement for a transfer + timer. (:pr:`NUMBER`) diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index ce51a4dea..35068e0ce 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -2,10 +2,16 @@ import logging import typing as t +import uuid -from globus_sdk import client, exc, response +from globus_sdk import _guards, client, exc, response from globus_sdk._types import UUIDLike -from globus_sdk.scopes import Scope, TimersScopes +from globus_sdk.scopes import ( + GCSCollectionScopeBuilder, + Scope, + TimersScopes, + TransferScopes, +) from .data import TimerJob, TransferTimer from .errors import TimersAPIError @@ -25,6 +31,73 @@ class TimersClient(client.BaseClient): scopes = TimersScopes default_scope_requirements = [Scope(TimersScopes.timer)] + def add_app_transfer_data_access_scope( + self, collection_ids: UUIDLike | t.Iterable[UUIDLike] + ) -> TimersClient: + """ + 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 resolving ``ConsentRequired`` errors when using Globus Connect + Server mapped collections. + + .. 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 + + 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.add_item("/staging/", "/active/") + + daily_timer = TransferTimer( + name="test_timer", schedule=RecurringTimerSchedule(24 * 60 * 60), body=transfer_data + ) + + client.create_timer(daily_timer) + """ # 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) + 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) + + timers_scope = Scope(TimersScopes.timer) + timers_scope.add_dependency(transfer_scope) + self.add_app_scope(timers_scope) + return self + def list_jobs( self, *, query_params: dict[str, t.Any] | None = None ) -> response.GlobusHTTPResponse: diff --git a/tests/unit/globus_app/test_client_integration.py b/tests/unit/globus_app/test_client_integration.py index c7f77daef..06998b00b 100644 --- a/tests/unit/globus_app/test_client_integration.py +++ b/tests/unit/globus_app/test_client_integration.py @@ -3,7 +3,7 @@ import pytest import globus_sdk -from globus_sdk import GlobusApp, GlobusAppConfig, GlobusSDKUsageError, UserApp +from globus_sdk import GlobusApp, GlobusAppConfig, UserApp from globus_sdk._testing import load_response from globus_sdk.tokenstorage import MemoryTokenStorage @@ -27,7 +27,7 @@ def test_client_environment_does_not_match_the_globus_app_environment(): config = GlobusAppConfig(token_storage=MemoryTokenStorage(), environment="sandbox") app = UserApp("test-app", client_id="client_id", config=config) - with pytest.raises(GlobusSDKUsageError) as exc: + with pytest.raises(globus_sdk.GlobusSDKUsageError) as exc: globus_sdk.AuthClient(app=app, environment="preview") expected = "[Environment Mismatch] AuthClient's environment (preview) does not match the GlobusApp's configured environment (sandbox)." # noqa: E501 @@ -52,6 +52,18 @@ def test_transfer_client_add_app_data_access_scope(app): assert expected in str_list +def test_timers_client_add_app_data_access_scope(app): + client = globus_sdk.TimersClient(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[globus_sdk.TimersClient.resource_server] + ] + expected = f"{globus_sdk.TimersClient.scopes.timer}[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)) @@ -89,6 +101,30 @@ def test_transfer_client_add_app_data_access_scope_in_iterable(app): assert (expected_2, True) in transfer_dependencies +def test_timers_client_add_app_data_access_scope_in_iterable(app): + collection_id_1 = str(uuid.UUID(int=1)) + collection_id_2 = str(uuid.UUID(int=2)) + globus_sdk.TimersClient(app=app).add_app_transfer_data_access_scope( + (collection_id_1, collection_id_2) + ) + + expected_1 = f"https://auth.globus.org/scopes/{collection_id_1}/data_access" + expected_2 = f"https://auth.globus.org/scopes/{collection_id_2}/data_access" + + transfer_dependencies = [] + for scope in app.scope_requirements[globus_sdk.TimersClient.resource_server]: + if scope.scope_string != globus_sdk.TimersClient.scopes.timer: + continue + for dep in scope.dependencies: + if dep.scope_string != globus_sdk.TransferClient.scopes.all: + continue + for subdep in dep.dependencies: + transfer_dependencies.append((subdep.scope_string, subdep.optional)) + + assert (expected_1, True) in transfer_dependencies + assert (expected_2, True) in transfer_dependencies + + def test_transfer_client_add_app_data_access_scope_catches_bad_uuid(app): with pytest.raises(ValueError, match="'collection_ids' must be a valid UUID"): globus_sdk.TransferClient(app=app).add_app_data_access_scope("foo") @@ -102,6 +138,19 @@ def test_transfer_client_add_app_data_access_scope_catches_bad_uuid_in_iterable( ) +def test_timers_client_add_app_data_access_scope_catches_bad_uuid(app): + with pytest.raises(ValueError, match="'collection_ids' must be a valid UUID"): + globus_sdk.TimersClient(app=app).add_app_transfer_data_access_scope("foo") + + +def test_timers_client_add_app_data_access_scope_catches_bad_uuid_in_iterable(app): + collection_id_1 = str(uuid.UUID(int=1)) + with pytest.raises(ValueError, match=r"'collection_ids\[1\]' must be a valid UUID"): + globus_sdk.TimersClient(app=app).add_app_transfer_data_access_scope( + [collection_id_1, "foo"] + ) + + def test_auth_client_default_scopes(app): globus_sdk.AuthClient(app=app) From ae6a24f5bdce9bed40a396ea196097fea5d03d40 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 4 Oct 2024 16:19:24 -0500 Subject: [PATCH 2/4] Add a new GlobusApp example using TimersClient This is a new (experimental) example, meant to replace the "timer_operations" examples with a more refined usage, based on GlobusApp. It relies on `TimersClient.add_app_transfer_data_access_scope`. --- .../examples/transferring_data/index.rst | 1 + .../timer_management/create_timer.py | 66 ++++++++++++++ .../create_timer_data_access.py | 89 +++++++++++++++++++ .../timer_management/delete_timer.py | 25 ++++++ .../timer_management/index.rst | 55 ++++++++++++ .../timer_management/list_timers.py | 24 +++++ 6 files changed, 260 insertions(+) create mode 100644 docs/experimental/examples/transferring_data/timer_management/create_timer.py create mode 100644 docs/experimental/examples/transferring_data/timer_management/create_timer_data_access.py create mode 100644 docs/experimental/examples/transferring_data/timer_management/delete_timer.py create mode 100644 docs/experimental/examples/transferring_data/timer_management/index.rst create mode 100644 docs/experimental/examples/transferring_data/timer_management/list_timers.py diff --git a/docs/experimental/examples/transferring_data/index.rst b/docs/experimental/examples/transferring_data/index.rst index d055fa84f..52839343c 100644 --- a/docs/experimental/examples/transferring_data/index.rst +++ b/docs/experimental/examples/transferring_data/index.rst @@ -6,3 +6,4 @@ Transferring Data :maxdepth: 1 submit_transfer/index + timer_management/index diff --git a/docs/experimental/examples/transferring_data/timer_management/create_timer.py b/docs/experimental/examples/transferring_data/timer_management/create_timer.py new file mode 100644 index 000000000..f988b08ab --- /dev/null +++ b/docs/experimental/examples/transferring_data/timer_management/create_timer.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +import argparse +import datetime + +import globus_sdk +from globus_sdk.experimental.globus_app import UserApp + +# Tutorial Client ID - +NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" +USER_APP = UserApp("manage-timers-example", client_id=NATIVE_CLIENT_ID) + + +def main(): + parser = argparse.ArgumentParser() + # the source, destination, and path to a file or dir to sync + parser.add_argument("SOURCE_COLLECTION") + parser.add_argument("DESTINATION_COLLECTION") + parser.add_argument("PATH") + parser.add_argument( + "--interval-seconds", + help="How frequently the timer runs, in seconds (default: 1 hour)", + default=3600, + type=int, + ) + parser.add_argument( + "--days", + help="How many days to run the timer (default: 2)", + default=2, + type=int, + ) + args = parser.parse_args() + + client = globus_sdk.TimersClient(app=USER_APP) + + body = globus_sdk.TransferData( + source_endpoint=args.SOURCE_COLLECTION, + destination_endpoint=args.DESTINATION_COLLECTION, + ) + body.add_item(args.PATH, args.PATH) + + # the timer will run until the end date, on whatever interval was requested + schedule = globus_sdk.RecurringTimerSchedule( + interval_seconds=args.interval_seconds, + end={ + "condition": "time", + "datetime": datetime.datetime.now() + datetime.timedelta(days=args.days), + }, + ) + + timer = client.create_timer( + timer=globus_sdk.TransferTimer( + name=( + "create-timer-example " + f"[created at {datetime.datetime.now().isoformat()}]" + ), + body=body, + schedule=schedule, + ) + ) + print("Finished submitting timer.") + print(f"timer_id: {timer['timer']['job_id']}") + + +if __name__ == "__main__": + main() diff --git a/docs/experimental/examples/transferring_data/timer_management/create_timer_data_access.py b/docs/experimental/examples/transferring_data/timer_management/create_timer_data_access.py new file mode 100644 index 000000000..56fd03dc3 --- /dev/null +++ b/docs/experimental/examples/transferring_data/timer_management/create_timer_data_access.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +import argparse +import datetime + +import globus_sdk +from globus_sdk.experimental.globus_app import UserApp + +# Tutorial Client ID - +NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" +USER_APP = UserApp("manage-timers-example", client_id=NATIVE_CLIENT_ID) + + +def uses_data_access(transfer_client, collection_id): + doc = transfer_client.get_endpoint(collection_id) + if doc["entity_type"] != "GCSv5_mapped_collection": + return False + if doc["high_assurance"]: + return False + return True + + +def main(): + parser = argparse.ArgumentParser() + # the source, destination, and path to a file or dir to sync + parser.add_argument("SOURCE_COLLECTION") + parser.add_argument("DESTINATION_COLLECTION") + parser.add_argument("PATH") + parser.add_argument( + "--interval-seconds", + help="How frequently the timer runs, in seconds (default: 1 hour)", + default=3600, + type=int, + ) + parser.add_argument( + "--days", + help="How many days to run the timer (default: 2)", + default=2, + type=int, + ) + args = parser.parse_args() + + timers_client = globus_sdk.TimersClient(app=USER_APP) + transfer_client = globus_sdk.TransferClient(app=USER_APP) + + # check if the source or destination use 'data_access' scopes + # if so, register these requirements with the app + if uses_data_access(transfer_client, args.SOURCE_COLLECTION): + timers_client.add_app_transfer_data_access_scope(args.SOURCE_COLLECTION) + if uses_data_access(transfer_client, args.DESTINATION_COLLECTION): + timers_client.add_app_transfer_data_access_scope(args.DESTINATION_COLLECTION) + + # from this point onwards, the example is the same as the basic create_timer.py + # script -- we've handled the nuance of data_access + # + # when the timer submission runs, you *may* be prompted to login again, if + # 'data_access' requirements were detected + + body = globus_sdk.TransferData( + source_endpoint=args.SOURCE_COLLECTION, + destination_endpoint=args.DESTINATION_COLLECTION, + ) + body.add_item(args.PATH, args.PATH) + + # the timer will run until the end date, on whatever interval was requested + schedule = globus_sdk.RecurringTimerSchedule( + interval_seconds=args.interval_seconds, + end={ + "condition": "time", + "datetime": datetime.datetime.now() + datetime.timedelta(days=args.days), + }, + ) + + timer = timers_client.create_timer( + timer=globus_sdk.TransferTimer( + name=( + "create-timer-example " + f"[created at {datetime.datetime.now().isoformat()}]" + ), + body=body, + schedule=schedule, + ) + ) + print("Finished submitting timer.") + print(f"timer_id: {timer['timer']['job_id']}") + + +if __name__ == "__main__": + main() diff --git a/docs/experimental/examples/transferring_data/timer_management/delete_timer.py b/docs/experimental/examples/transferring_data/timer_management/delete_timer.py new file mode 100644 index 000000000..560acd831 --- /dev/null +++ b/docs/experimental/examples/transferring_data/timer_management/delete_timer.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import argparse + +import globus_sdk +from globus_sdk.experimental.globus_app import UserApp + +# Tutorial Client ID - +NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" +USER_APP = UserApp("manage-timers-example", client_id=NATIVE_CLIENT_ID) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("TIMER_ID") + args = parser.parse_args() + + client = globus_sdk.TimersClient(app=USER_APP) + + client.delete_job(args.TIMER_ID) + print("Finished deleting timer.") + + +if __name__ == "__main__": + main() diff --git a/docs/experimental/examples/transferring_data/timer_management/index.rst b/docs/experimental/examples/transferring_data/timer_management/index.rst new file mode 100644 index 000000000..4028cb7a0 --- /dev/null +++ b/docs/experimental/examples/transferring_data/timer_management/index.rst @@ -0,0 +1,55 @@ +Timer Management +---------------- + +These examples demonstrate how to create, list, and delete timers with the SDK. + +Create a timer +~~~~~~~~~~~~~~ + +This script creates a new timer, on source and destination collections provided +via the command-line. It syncs an input file or directory between the two. + +.. note:: + This example does not handle ``data_access`` scope requirements. + See the later example to handle this. + +.. literalinclude:: create_timer.py + :caption: ``create_timer.py`` [:download:`download `] + :language: python + +List timers +~~~~~~~~~~~ + +This script lists your current timers. + +.. literalinclude:: list_timers.py + :caption: ``list_timers.py`` [:download:`download `] + :language: python + +Delete a timer +~~~~~~~~~~~~~~ + +This script creates a new timer, on source and destination collections provided +via the command-line. It syncs an input file or directory between the two. + +.. literalinclude:: delete_timer.py + :caption: ``delete_timer.py`` [:download:`download `] + :language: python + +Create a timer with ``data_access`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This script is similar to the ``create_timer.py`` example above. However, it +also handles ``data_access`` scope requirements for the source and destination +collections. + +Discovering ``data_access`` requirements requires the use of a +``TransferClient`` to look up the collections. + +As in the simpler example, this script creates a new timer, on source and +destination collections provided via the command-line. It syncs an input +file or directory between the two. + +.. literalinclude:: create_timer_data_access.py + :caption: ``create_timer_data_access.py`` [:download:`download `] + :language: python diff --git a/docs/experimental/examples/transferring_data/timer_management/list_timers.py b/docs/experimental/examples/transferring_data/timer_management/list_timers.py new file mode 100644 index 000000000..a100e0a8f --- /dev/null +++ b/docs/experimental/examples/transferring_data/timer_management/list_timers.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import globus_sdk +from globus_sdk.experimental.globus_app import UserApp + +# Tutorial Client ID - +NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" +USER_APP = UserApp("manage-timers-example", client_id=NATIVE_CLIENT_ID) + + +def main(): + client = globus_sdk.TimersClient(app=USER_APP) + + first = True + for record in client.list_jobs(query_params={"filter_active": True})["jobs"]: + if not first: + print("---") + first = False + print("name:", record["name"]) + print("id:", record["job_id"]) + + +if __name__ == "__main__": + main() From 16dab87cc14c2e30428a1d7fa29ff297d50a43e4 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 22 Oct 2024 11:28:50 -0500 Subject: [PATCH 3/4] Promote the "experimental" timer example doc This moves the doc into non-experimental examples, and replaces the older 'timer_operations' example it was written to replace. --- docs/examples/index.rst | 2 +- .../timer_management/create_timer.py | 0 .../create_timer_data_access.py | 0 .../timer_management/delete_timer.py | 0 .../timer_management/index.rst | 2 + .../timer_management/list_timers.py | 0 docs/examples/timer_operations.rst | 4 + .../examples/timer_operations/create_timer.py | 88 ---------- .../create_timer_data_access.py | 150 ------------------ .../examples/timer_operations/delete_timer.py | 47 ------ docs/examples/timer_operations/index.rst | 47 ------ .../examples/transferring_data/index.rst | 1 - 12 files changed, 7 insertions(+), 334 deletions(-) rename docs/{experimental/examples/transferring_data => examples}/timer_management/create_timer.py (100%) rename docs/{experimental/examples/transferring_data => examples}/timer_management/create_timer_data_access.py (100%) rename docs/{experimental/examples/transferring_data => examples}/timer_management/delete_timer.py (100%) rename docs/{experimental/examples/transferring_data => examples}/timer_management/index.rst (98%) rename docs/{experimental/examples/transferring_data => examples}/timer_management/list_timers.py (100%) create mode 100644 docs/examples/timer_operations.rst delete mode 100644 docs/examples/timer_operations/create_timer.py delete mode 100644 docs/examples/timer_operations/create_timer_data_access.py delete mode 100644 docs/examples/timer_operations/delete_timer.py delete mode 100644 docs/examples/timer_operations/index.rst diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 552e9ab29..e1c700c34 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -18,5 +18,5 @@ Each of these pages contains an example of a piece of SDK functionality. transfer_relative_deadlines recursive_ls endpoint_type_enum - timer_operations/index + timer_management/index guest_collection_creation diff --git a/docs/experimental/examples/transferring_data/timer_management/create_timer.py b/docs/examples/timer_management/create_timer.py similarity index 100% rename from docs/experimental/examples/transferring_data/timer_management/create_timer.py rename to docs/examples/timer_management/create_timer.py diff --git a/docs/experimental/examples/transferring_data/timer_management/create_timer_data_access.py b/docs/examples/timer_management/create_timer_data_access.py similarity index 100% rename from docs/experimental/examples/transferring_data/timer_management/create_timer_data_access.py rename to docs/examples/timer_management/create_timer_data_access.py diff --git a/docs/experimental/examples/transferring_data/timer_management/delete_timer.py b/docs/examples/timer_management/delete_timer.py similarity index 100% rename from docs/experimental/examples/transferring_data/timer_management/delete_timer.py rename to docs/examples/timer_management/delete_timer.py diff --git a/docs/experimental/examples/transferring_data/timer_management/index.rst b/docs/examples/timer_management/index.rst similarity index 98% rename from docs/experimental/examples/transferring_data/timer_management/index.rst rename to docs/examples/timer_management/index.rst index 4028cb7a0..7e546ecc7 100644 --- a/docs/experimental/examples/transferring_data/timer_management/index.rst +++ b/docs/examples/timer_management/index.rst @@ -1,3 +1,5 @@ +.. _timer_management_examples: + Timer Management ---------------- diff --git a/docs/experimental/examples/transferring_data/timer_management/list_timers.py b/docs/examples/timer_management/list_timers.py similarity index 100% rename from docs/experimental/examples/transferring_data/timer_management/list_timers.py rename to docs/examples/timer_management/list_timers.py diff --git a/docs/examples/timer_operations.rst b/docs/examples/timer_operations.rst new file mode 100644 index 000000000..5b8619cab --- /dev/null +++ b/docs/examples/timer_operations.rst @@ -0,0 +1,4 @@ +:orphan: + +The documentation which was found on this page has moved to +:ref:`Timer Management Examples `. diff --git a/docs/examples/timer_operations/create_timer.py b/docs/examples/timer_operations/create_timer.py deleted file mode 100644 index 8cdac7abd..000000000 --- a/docs/examples/timer_operations/create_timer.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python - -import argparse -import datetime - -import globus_sdk - -# tutorial client ID -# we recommend replacing this with your own client for any production use-cases -CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" -NATIVE_CLIENT = globus_sdk.NativeAppAuthClient(CLIENT_ID) - - -def do_login_flow(): - # we will want to request a 'timer' scope for managing timers - scope = globus_sdk.TimersClient.scopes.timer - - # run the login flow, finishing with a token exchange - NATIVE_CLIENT.oauth2_start_flow(requested_scopes=scope) - authorize_url = NATIVE_CLIENT.oauth2_get_authorize_url() - print(f"Please go to this URL and login:\n\n{authorize_url}\n") - auth_code = input("Please enter the code here: ").strip() - tokens = NATIVE_CLIENT.oauth2_exchange_code_for_tokens(auth_code) - - # pull out the tokens for Globus Timers from the response - return tokens.by_resource_server[globus_sdk.TimersClient.resource_server] - - -def create_timers_client(): - tokens = do_login_flow() - return globus_sdk.TimersClient( - authorizer=globus_sdk.AccessTokenAuthorizer(tokens["access_token"]) - ) - - -def main(): - parser = argparse.ArgumentParser() - # the source, destination, and path to a file or dir to sync - parser.add_argument("SOURCE_COLLECTION") - parser.add_argument("DESTINATION_COLLECTION") - parser.add_argument("PATH") - parser.add_argument( - "--interval-seconds", - help="How frequently the timer runs, in seconds (default: 1 hour)", - default=3600, - type=int, - ) - parser.add_argument( - "--days", - help="How many days to run the timer (default: 2)", - default=2, - type=int, - ) - args = parser.parse_args() - - client = create_timers_client() - - body = globus_sdk.TransferData( - source_endpoint=args.SOURCE_COLLECTION, - destination_endpoint=args.DESTINATION_COLLECTION, - ) - body.add_item(args.PATH, args.PATH) - - # the timer will run until the end date, on whatever interval was requested - schedule = globus_sdk.RecurringTimerSchedule( - interval_seconds=args.interval_seconds, - end={ - "condition": "time", - "datetime": datetime.datetime.now() + datetime.timedelta(days=args.days), - }, - ) - - timer = client.create_timer( - timer=globus_sdk.TransferTimer( - name=( - "create-timer-example " - f"[created at {datetime.datetime.now().isoformat()}]" - ), - body=body, - schedule=schedule, - ) - ) - print("Finished submitting timer.") - print(f"timer_id: {timer['timer']['job_id']}") - - -if __name__ == "__main__": - main() diff --git a/docs/examples/timer_operations/create_timer_data_access.py b/docs/examples/timer_operations/create_timer_data_access.py deleted file mode 100644 index 5ad9d1545..000000000 --- a/docs/examples/timer_operations/create_timer_data_access.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python - -import argparse -import datetime - -import globus_sdk - -# tutorial client ID -# we recommend replacing this with your own client for any production use-cases -CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" -NATIVE_CLIENT = globus_sdk.NativeAppAuthClient(CLIENT_ID) - - -def uses_data_access(transfer_client, collection_id): - doc = transfer_client.get_endpoint(collection_id) - if doc["entity_type"] != "GCSv5_mapped_collection": - return False - if doc["high_assurance"]: - return False - return True - - -def get_data_access_scopes(transfer_client, collection_ids): - data_access_scopes = [] - for collection_id in collection_ids: - if uses_data_access(transfer_client, collection_id): - data_access_scopes.append( - globus_sdk.GCSClient.get_gcs_collection_scopes( - collection_id - ).data_access - ) - return data_access_scopes - - -def do_login_flow(data_access_scopes=None): - # we will want to request a 'timer' scope for managing timers - # and a 'transfer:all' scope for inspecting collections - # - # if there are data_access scopes to request, we'll need to 'enhance' the Timers - # scope to be shaped like this: - # - # timers_scope -> - # transfer_scope -> - # data_access1 - # data_access2 - # - # this scope structure encodes permission for Timers to use Transfer on the - # target collections - timer_scope = globus_sdk.TimersClient.scopes.timer - if data_access_scopes: - transfer_scope = globus_sdk.Scope(globus_sdk.TransferClient.scopes.all) - for da_scope in data_access_scopes: - transfer_scope.add_dependency(da_scope) - - timer_scope = globus_sdk.Scope(globus_sdk.TimersClient.scopes.timer) - timer_scope.add_dependency(transfer_scope) - - scopes = [ - timer_scope, - globus_sdk.TransferClient.scopes.all, - ] - - # run the login flow, finishing with a token exchange - NATIVE_CLIENT.oauth2_start_flow(requested_scopes=scopes) - authorize_url = NATIVE_CLIENT.oauth2_get_authorize_url() - print(f"Please go to this URL and login:\n\n{authorize_url}\n") - auth_code = input("Please enter the code here: ").strip() - tokens = NATIVE_CLIENT.oauth2_exchange_code_for_tokens(auth_code) - - # return Transfer and Timers tokens - return tokens.by_resource_server - - -def create_clients(data_access_scopes=None): - tokens = do_login_flow(data_access_scopes=data_access_scopes) - timers_tokens = tokens[globus_sdk.TimersClient.resource_server] - transfer_tokens = tokens[globus_sdk.TransferClient.resource_server] - - timers_client = globus_sdk.TimersClient( - authorizer=globus_sdk.AccessTokenAuthorizer(timers_tokens["access_token"]) - ) - transfer_client = globus_sdk.TransferClient( - authorizer=globus_sdk.AccessTokenAuthorizer(transfer_tokens["access_token"]) - ) - return timers_client, transfer_client - - -def main(): - parser = argparse.ArgumentParser() - # the source, destination, and path to a file or dir to sync - parser.add_argument("SOURCE_COLLECTION") - parser.add_argument("DESTINATION_COLLECTION") - parser.add_argument("PATH") - parser.add_argument( - "--interval-seconds", - help="How frequently the timer runs, in seconds (default: 1 hour)", - default=3600, - type=int, - ) - parser.add_argument( - "--days", - help="How many days to run the timer (default: 2)", - default=2, - type=int, - ) - args = parser.parse_args() - - # login and get relevant clients, but also check if we need to re-login for - # data_access and potentially replace the timers_client as a result - timers_client, transfer_client = create_clients() - data_access_scopes = get_data_access_scopes( - transfer_client, [args.SOURCE_COLLECTION, args.DESTINATION_COLLECTION] - ) - if data_access_scopes: - timers_client, _ = create_clients(data_access_scopes=data_access_scopes) - - # from this point onwards, the example is the same as the basic create_timer.py - # script -- we've handled the nuance of data_access - - body = globus_sdk.TransferData( - source_endpoint=args.SOURCE_COLLECTION, - destination_endpoint=args.DESTINATION_COLLECTION, - ) - body.add_item(args.PATH, args.PATH) - - # the timer will run until the end date, on whatever interval was requested - schedule = globus_sdk.RecurringTimerSchedule( - interval_seconds=args.interval_seconds, - end={ - "condition": "time", - "datetime": datetime.datetime.now() + datetime.timedelta(days=args.days), - }, - ) - - timer = timers_client.create_timer( - timer=globus_sdk.TransferTimer( - name=( - "create-timer-example " - f"[created at {datetime.datetime.now().isoformat()}]" - ), - body=body, - schedule=schedule, - ) - ) - print("Finished submitting timer.") - print(f"timer_id: {timer['timer']['job_id']}") - - -if __name__ == "__main__": - main() diff --git a/docs/examples/timer_operations/delete_timer.py b/docs/examples/timer_operations/delete_timer.py deleted file mode 100644 index 46f5d9729..000000000 --- a/docs/examples/timer_operations/delete_timer.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -import argparse - -import globus_sdk - -# tutorial client ID -# we recommend replacing this with your own client for any production use-cases -CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2" -NATIVE_CLIENT = globus_sdk.NativeAppAuthClient(CLIENT_ID) - - -def do_login_flow(): - # we will want to request a 'timer' scope for managing timers - scope = globus_sdk.TimersClient.scopes.timer - - # run the login flow, finishing with a token exchange - NATIVE_CLIENT.oauth2_start_flow(requested_scopes=scope) - authorize_url = NATIVE_CLIENT.oauth2_get_authorize_url() - print(f"Please go to this URL and login:\n\n{authorize_url}\n") - auth_code = input("Please enter the code here: ").strip() - tokens = NATIVE_CLIENT.oauth2_exchange_code_for_tokens(auth_code) - - # pull out the tokens for Globus Timers from the response - return tokens.by_resource_server[globus_sdk.TimersClient.resource_server] - - -def create_timers_client(): - tokens = do_login_flow() - return globus_sdk.TimersClient( - authorizer=globus_sdk.AccessTokenAuthorizer(tokens["access_token"]) - ) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("TIMER_ID") - args = parser.parse_args() - - client = create_timers_client() - - client.delete_timer(args.TIMER_ID) - print("Finished deleting timer.") - - -if __name__ == "__main__": - main() diff --git a/docs/examples/timer_operations/index.rst b/docs/examples/timer_operations/index.rst deleted file mode 100644 index 2f06c9e2a..000000000 --- a/docs/examples/timer_operations/index.rst +++ /dev/null @@ -1,47 +0,0 @@ -Globus Timers Operations ------------------------- - -These examples demonstrate how to create, list, and delete Timers with the SDK. - -Create a timer -~~~~~~~~~~~~~~ - -This script creates a new timer, on source and destination collections provided -via the command-line. It syncs an input file or directory between the two. - -.. note:: - This example does not handle ``data_access`` scope requirements. - See the later example to handle this. - -.. literalinclude:: create_timer.py - :caption: ``create_timer.py`` [:download:`download `] - :language: python - -Delete a timer -~~~~~~~~~~~~~~ - -This script creates a new timer, on source and destination collections provided -via the command-line. It syncs an input file or directory between the two. - -.. literalinclude:: delete_timer.py - :caption: ``delete_timer.py`` [:download:`download `] - :language: python - -Create a timer with ``data_access`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This script is similar to the ``create_timer.py`` example above. However, it -also handles ``data_access`` scope requirements for the source and destination -collections. - -Discovering ``data_access`` requirements requires the use of a -``TransferClient`` to look up the collections. Therefore, this example may put -the user through two login flows. - -As in the simpler example, this script creates a new timer, on source and -destination collections provided via the command-line. It syncs an input -file or directory between the two. - -.. literalinclude:: create_timer_data_access.py - :caption: ``create_timer_data_access.py`` [:download:`download `] - :language: python diff --git a/docs/experimental/examples/transferring_data/index.rst b/docs/experimental/examples/transferring_data/index.rst index 52839343c..d055fa84f 100644 --- a/docs/experimental/examples/transferring_data/index.rst +++ b/docs/experimental/examples/transferring_data/index.rst @@ -6,4 +6,3 @@ Transferring Data :maxdepth: 1 submit_transfer/index - timer_management/index From 7b1321f6cdaebc8a03471692d086ef5635e7d399 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 24 Oct 2024 09:26:25 -0500 Subject: [PATCH 4/4] Update Timer Management examples per review Co-authored-by: Ada <107940310+ada-globus@users.noreply.github.com> --- docs/examples/timer_management/index.rst | 9 ++++++--- src/globus_sdk/services/timers/client.py | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/examples/timer_management/index.rst b/docs/examples/timer_management/index.rst index 7e546ecc7..34ad7f0f6 100644 --- a/docs/examples/timer_management/index.rst +++ b/docs/examples/timer_management/index.rst @@ -11,6 +11,9 @@ Create a timer This script creates a new timer, on source and destination collections provided via the command-line. It syncs an input file or directory between the two. +The script assumes that the path being synced is the same on the source and +destination for simplicity. + .. note:: This example does not handle ``data_access`` scope requirements. See the later example to handle this. @@ -31,8 +34,7 @@ This script lists your current timers. Delete a timer ~~~~~~~~~~~~~~ -This script creates a new timer, on source and destination collections provided -via the command-line. It syncs an input file or directory between the two. +This script deletes a timer by ID. .. literalinclude:: delete_timer.py :caption: ``delete_timer.py`` [:download:`download `] @@ -50,7 +52,8 @@ Discovering ``data_access`` requirements requires the use of a As in the simpler example, this script creates a new timer, on source and destination collections provided via the command-line. It syncs an input -file or directory between the two. +file or directory between the two, and assumes that the path is the same on the +source and destination. .. literalinclude:: create_timer_data_access.py :caption: ``create_timer_data_access.py`` [:download:`download `] diff --git a/src/globus_sdk/services/timers/client.py b/src/globus_sdk/services/timers/client.py index 35068e0ce..a8ac2acc8 100644 --- a/src/globus_sdk/services/timers/client.py +++ b/src/globus_sdk/services/timers/client.py @@ -37,8 +37,9 @@ def add_app_transfer_data_access_scope( """ 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 resolving ``ConsentRequired`` errors when using Globus Connect - Server mapped collections. + Useful for preventing ``ConsentRequired`` errors when creating timers + that use Globus Connect Server mapped collection(s) as the source or + destination. .. warning::