Skip to content

Commit 1fab1a6

Browse files
authored
Update the minimal transfer scripts examples (#1419)
2 parents 68229cc + e62b24b commit 1fab1a6

4 files changed

Lines changed: 90 additions & 208 deletions

File tree

docs/examples/minimal_transfer_script/index.rst

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ The following is an extremely minimal script to demonstrate a file transfer
1010
using the :class:`TransferClient <globus_sdk.TransferClient>`.
1111

1212
It uses the tutorial client ID from the :ref:`tutorials <tutorials>`.
13-
For simplicity, the script will prompt for login on each use.
1413

1514
.. note::
1615
You will need to replace the values for ``source_collection_id`` and
@@ -20,47 +19,35 @@ For simplicity, the script will prompt for login on each use.
2019
:caption: ``transfer_minimal.py`` [:download:`download <transfer_minimal.py>`]
2120
:language: python
2221

23-
24-
Minimal File Transfer Script Handling ConsentRequired
25-
-----------------------------------------------------
26-
27-
The above example works with certain endpoint types, but will fail if either
28-
the source or destination endpoint requires a ``data_access`` scope. This
29-
requirement will cause the Transfer submission to fail with a
30-
``ConsentRequired`` error.
31-
32-
The example below catches the ``ConsentRequired`` error and retries the
33-
submission after a second login.
34-
35-
This kind of "reactive" handling of ``ConsentRequired`` is the simplest
36-
strategy to design and implement.
37-
38-
We'll also enhance the example to take endpoint IDs from the command line.
39-
40-
.. literalinclude:: transfer_consent_required_reactive.py
41-
:caption: ``transfer_consent_required_reactive.py`` [:download:`download <transfer_consent_required_reactive.py>`]
42-
:language: python
43-
44-
4522
Best-Effort Proactive Handling of ConsentRequired
4623
-------------------------------------------------
4724

4825
The above example works in most cases, and especially when there is a low cost
49-
to failing and retrying an activity.
26+
to failing and retrying an activity. The ``auto_redrive_gares`` flag enables a
27+
behavior which will prompt the user for a fresh login if they are missing
28+
consents for access to various collections.
5029

51-
However, in some cases, responding to ``ConsentRequired`` errors when the task
52-
is submitted is not acceptable. For example, for scripts used in batch job
53-
systems, the user cannot respond to the error until the job is already
54-
executing. The user would rather handle such issues when submitting their job.
30+
However, in some cases, responding to missing consents when the task is
31+
submitted is not acceptable. For example, for scripts used in batch job systems,
32+
the user cannot respond to the error until the job is already executing. The
33+
user would rather handle such issues when submitting their job.
5534

56-
``ConsentRequired`` errors in this case can be avoided on a best-effort basis.
57-
Note, however, that the process for consenting ahead of time is more error
58-
prone and complex.
35+
The service still relies on ``ConsentRequired`` errors to indicate that some
36+
additional user consent is needed. But we can intentionally trigger them early
37+
to control when the user is prompted to resolve them.
38+
39+
The example below tries an ``ls`` operation before starting to build the task
40+
data. If the ``ls`` fails with ``ConsentRequired``, the user can be put through
41+
the relevant login flow. Other errors (e.g., bad permissions) are suppressed, as
42+
they probably aren't relevant to the user.
43+
44+
.. note::
45+
The ``UserApp`` object is instantiated a second time, later in the script, to
46+
actually start the transfer. This loads the same tokens from the earlier login
47+
via the default token storage in ``~/.globus/``.
5948

60-
The example below enhances the previous reactive error handling to try an
61-
``ls`` operation before starting to build the task data. If the ``ls`` fails
62-
with ``ConsentRequired``, the user can be put through the relevant login flow.
63-
And if not, we can relatively safely assume that any errors are not relevant.
49+
To manage tokens in another way, please see the documentation on
50+
:ref:`Token Storages <token_storages>`.
6451

6552
.. literalinclude:: transfer_consent_required_proactive.py
6653
:caption: ``transfer_consent_required_proactive.py`` [:download:`download <transfer_consent_required_proactive.py>`]

docs/examples/minimal_transfer_script/transfer_consent_required_proactive.py

Lines changed: 52 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,47 @@
33
import globus_sdk
44
from globus_sdk.scopes import TransferScopes
55

6+
# do basic argument parsing
67
parser = argparse.ArgumentParser()
78
parser.add_argument("SRC")
89
parser.add_argument("DST")
910
args = parser.parse_args()
1011

12+
# tutorial client ID (we recommend replacing this with your own client)
1113
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
12-
auth_client = globus_sdk.NativeAppAuthClient(CLIENT_ID)
14+
APP_NAME = "proactive-transfer-consent-example"
1315

1416

15-
# we will need to do the login flow potentially twice, so define it as a
16-
# function
17+
# Try an ls on the source and destination to see if ConsentRequired errors are raised --
18+
# if they are, a fresh login flow will *not* be triggered.
1719
#
18-
# we default to using the Transfer "all" scope, but it is settable here
19-
# look at the ConsentRequired handler below for how this is used
20-
def login_and_get_transfer_client(*, scopes=TransferScopes.all):
21-
# note that 'requested_scopes' can be a single scope or a list
22-
# this did not matter in previous examples but will be leveraged in
23-
# this one
24-
auth_client.oauth2_start_flow(requested_scopes=scopes)
25-
authorize_url = auth_client.oauth2_get_authorize_url()
26-
print(f"Please go to this URL and login:\n\n{authorize_url}\n")
27-
28-
auth_code = input("Please enter the code here: ").strip()
29-
tokens = auth_client.oauth2_exchange_code_for_tokens(auth_code)
30-
transfer_tokens = tokens.by_resource_server["transfer.api.globus.org"]
31-
32-
# return the TransferClient object, as the result of doing a login
33-
return globus_sdk.TransferClient(
34-
authorizer=globus_sdk.AccessTokenAuthorizer(transfer_tokens["access_token"])
35-
)
36-
37-
38-
# get an initial client to try with, which requires a login flow
39-
transfer_client = login_and_get_transfer_client()
40-
41-
# now, try an ls on the source and destination to see if ConsentRequired
42-
# errors are raised
43-
consent_required_scopes = []
44-
45-
46-
def check_for_consent_required(target):
47-
try:
48-
transfer_client.operation_ls(target, path="/")
49-
# catch all errors and discard those other than ConsentRequired
50-
# e.g. ignore PermissionDenied errors as not relevant
51-
except globus_sdk.TransferAPIError as err:
52-
if err.info.consent_required:
53-
consent_required_scopes.extend(err.info.consent_required.required_scopes)
54-
55-
56-
check_for_consent_required(args.SRC)
57-
check_for_consent_required(args.DST)
20+
# This is more sophisticated than handling with `redrive_gares=True` and makes
21+
# sure that the user is only prompted to login *one* extra time, even if both
22+
# collections require additional consent.
23+
def probe_for_consent_required(
24+
transfer_client: globus_sdk.TransferClient, targets: list[str]
25+
) -> list[str]:
26+
consent_required_scopes: list[str] = []
27+
28+
for target in targets:
29+
try:
30+
transfer_client.operation_ls(target, path="/")
31+
# catch all errors and discard those other than ConsentRequired
32+
# e.g. ignore PermissionDenied errors as not relevant
33+
except globus_sdk.TransferAPIError as err:
34+
if err.info.consent_required:
35+
consent_required_scopes.extend(
36+
err.info.consent_required.required_scopes
37+
)
38+
39+
return consent_required_scopes
40+
41+
42+
with globus_sdk.UserApp(APP_NAME, client_id=CLIENT_ID) as app:
43+
with globus_sdk.TransferClient(app=app) as transfer_client:
44+
consent_required_scopes = probe_for_consent_required(
45+
transfer_client, [args.SRC, args.DST]
46+
)
5847

5948
# the block above may or may not populate this list
6049
# but if it does, handle ConsentRequired with a new login
@@ -63,15 +52,21 @@ def check_for_consent_required(target):
6352
"One of your endpoints requires consent in order to be used.\n"
6453
"You must login a second time to grant consents.\n\n"
6554
)
66-
transfer_client = login_and_get_transfer_client(scopes=consent_required_scopes)
67-
68-
# from this point onwards, the example is exactly the same as the reactive
69-
# case, including the behavior to retry on ConsentRequiredErrors. This is
70-
# not obvious, but there are cases in which it is necessary -- for example,
71-
# if a user consents at the start, but the process of building task_data is
72-
# slow, they could revoke their consent before the submission step
73-
#
74-
# in the common case, a single submission with no retry would suffice
55+
with globus_sdk.UserApp(
56+
APP_NAME,
57+
client_id=CLIENT_ID,
58+
scope_requirements={
59+
TransferScopes.resource_server: consent_required_scopes
60+
+ [TransferScopes.all]
61+
},
62+
) as app:
63+
app.login()
64+
65+
66+
# From this point onwards, the example is exactly the same as the previous scripts.
67+
# We will *not* set `redrive_gares=True`, on the grounds that if you want to use this
68+
# in a context like a job submission system, a prompt for login is not helpful if the
69+
# consent was revoked or insufficient.
7570

7671
task_data = globus_sdk.TransferData(
7772
source_endpoint=args.SRC, destination_endpoint=args.DST
@@ -81,23 +76,9 @@ def check_for_consent_required(target):
8176
"/~/example-transfer-script-destination.txt", # dest
8277
)
8378

79+
with globus_sdk.UserApp(APP_NAME, client_id=CLIENT_ID) as app:
80+
with globus_sdk.TransferClient(app=app) as transfer_client:
81+
task_doc = transfer_client.submit_transfer(task_data)
8482

85-
def do_submit(client):
86-
task_doc = client.submit_transfer(task_data)
87-
task_id = task_doc["task_id"]
88-
print(f"submitted transfer, task_id={task_id}")
89-
90-
91-
try:
92-
do_submit(transfer_client)
93-
except globus_sdk.TransferAPIError as err:
94-
if not err.info.consent_required:
95-
raise
96-
print(
97-
"Encountered a ConsentRequired error.\n"
98-
"You must login a second time to grant consents.\n\n"
99-
)
100-
transfer_client = login_and_get_transfer_client(
101-
scopes=err.info.consent_required.required_scopes
102-
)
103-
do_submit(transfer_client)
83+
task_id = task_doc["task_id"]
84+
print(f"submitted transfer, task_id={task_id}")

docs/examples/minimal_transfer_script/transfer_consent_required_reactive.py

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,30 @@
11
import globus_sdk
2-
from globus_sdk.scopes import TransferScopes
32

3+
# tutorial client ID (we recommend replacing this with your own client)
44
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
5-
auth_client = globus_sdk.NativeAppAuthClient(CLIENT_ID)
6-
7-
# requested_scopes specifies a list of scopes to request
8-
# instead of the defaults, only request access to the Transfer API
9-
auth_client.oauth2_start_flow(requested_scopes=TransferScopes.all)
10-
authorize_url = auth_client.oauth2_get_authorize_url()
11-
print(f"Please go to this URL and login:\n\n{authorize_url}\n")
12-
13-
auth_code = input("Please enter the code here: ").strip()
14-
tokens = auth_client.oauth2_exchange_code_for_tokens(auth_code)
15-
transfer_tokens = tokens.by_resource_server["transfer.api.globus.org"]
16-
17-
# construct an AccessTokenAuthorizer and use it to construct the
18-
# TransferClient
19-
transfer_client = globus_sdk.TransferClient(
20-
authorizer=globus_sdk.AccessTokenAuthorizer(transfer_tokens["access_token"])
21-
)
225

236
# Replace these with your own collection UUIDs
24-
source_collection_id = "..."
25-
dest_collection_id = "..."
7+
SOURCE_COLLECTION_ID = "..."
8+
DEST_COLLECTION_ID = "..."
269

2710
# create a Transfer task consisting of one or more items
28-
task_data = globus_sdk.TransferData(
29-
source_endpoint=source_collection_id, destination_endpoint=dest_collection_id
30-
)
11+
task_data = globus_sdk.TransferData(SOURCE_COLLECTION_ID, DEST_COLLECTION_ID)
3112
task_data.add_item(
3213
"/share/godata/file1.txt", # source
3314
"/~/minimal-example-transfer-script-destination.txt", # dest
3415
)
3516

36-
# submit, getting back the task ID
37-
task_doc = transfer_client.submit_transfer(task_data)
17+
# create an app to manage login, use it to create a client, and submit,
18+
# getting back the task ID
19+
with globus_sdk.UserApp(
20+
"minimal-transfer-example",
21+
client_id=CLIENT_ID,
22+
# we set the 'auto_redrive_gares' flag, which enables handling for missing
23+
# auth requirements when the script is run against a changing set of collection IDs
24+
config=globus_sdk.GlobusAppConfig(auto_redrive_gares=True),
25+
) as app:
26+
with globus_sdk.TransferClient(app=app) as transfer_client:
27+
task_doc = transfer_client.submit_transfer(task_data)
28+
3829
task_id = task_doc["task_id"]
3930
print(f"submitted transfer, task_id={task_id}")

0 commit comments

Comments
 (0)