Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions changelog.d/20251002_135923_sirosen_resource_ownership.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Added
-----

- ``GlobusApp`` usages now support resource tracking in which they will close
the transports used by registered clients and the underlying token storage
when via a ``close()`` method. (:pr:`NUMBER`)

- ``GlobusApp`` now features a ``close()`` method for doing imperative
resource cleanup.

- ``GlobusApp`` can be used as a context manager, and calls ``close()`` on
exit.

- Transports which are created by clients, and token storages which
are created by apps, are subject to this cleanup behavior. Imperatively
created ones which are passed in to clients and apps are not.

- The SDK internally tracks the relationships between apps, clients,
transports, and token storages for these purposes.
`Weak references <docs.python.org/3/library/weakref.html>`_ are used to
minimize the impact of resource tracking on garbage collection.
51 changes: 51 additions & 0 deletions docs/authorization/globus_app/apps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,57 @@ The following table provides a comparison of these two options:
a user be the primary data access actor. In these cases, a ``ClientApp``
will be rejected and a ``UserApp`` must be used instead.


Closing Resources via GlobusApps
--------------------------------

When used as context managers, ``GlobusApp``\s automatically call their
``close()`` method on exit.

Closing an app closes the resources owned by it.
This covers any token storage created by the app on init, and any clients which
are registered with the app.

When token storages and transports are created explicitly, they are exempt from
the close behavior.
For example,

.. code-block:: python

from globus_sdk import GlobusAppConfig, UserApp
from globus_sdk.token_storage import SQLiteTokenStorage

# this manually created storage will not be automatically closed
sql_storage = SQLiteTokenStorage("tokens.sqlite")

# create an app configured to use this storage
config = GlobusAppConfig(token_storage=sql_storage)
app = UserApp("sample-app", client_id="FILL_IN_HERE", config=config)

# close the app
app.close()

# at this stage, the storage will still not be closed
# it can be explicitly closed
sql_storage.close()

For most use-cases, users are recommended to use the context manager form, which
ensures that ``close()`` will be called, and to allow ``GlobusApp`` to maintain ownership
of token storage.
For example,

.. code-block:: python

from globus_sdk import GlobusAppConfig, UserApp

# create an app configured to create its own sqlite storage
config = GlobusAppConfig(token_storage="sqlite")
with UserApp("sample-app", client_id="FILL_IN_HERE", config=config) as app:
... # any clients, usage, etc.

# after the context manager, any storage is implicitly closed


Reference
---------

Expand Down
27 changes: 13 additions & 14 deletions docs/user_guide/getting_started/list_groups_with_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"

# create your app
my_app = globus_sdk.UserApp("my-user-app", client_id=CLIENT_ID)
with globus_sdk.UserApp("my-user-app", client_id=CLIENT_ID) as my_app:
# create a client with your app
groups_client = globus_sdk.GroupsClient(app=my_app)

# create a client with your app
groups_client = globus_sdk.GroupsClient(app=my_app)
# Important! The login step needs to happen after the `groups_client` is created
# so that the app will know that you need credentials for Globus Groups
my_app.login()

# Important! The login step needs to happen after the `groups_client` is created
# so that the app will know that you need credentials for Globus Groups
my_app.login()
# call out to the Groups service to get a listing
my_groups = groups_client.get_my_groups()

# call out to the Groups service to get a listing
my_groups = groups_client.get_my_groups()

# print in CSV format
print("ID,Name,Roles")
for group in my_groups:
roles = "|".join({m["role"] for m in group["my_memberships"]})
print(",".join([group["id"], f'"{group["name"]}"', roles]))
# print in CSV format
print("ID,Name,Roles")
for group in my_groups:
roles = "|".join({m["role"] for m in group["my_memberships"]})
print(",".join([group["id"], f'"{group["name"]}"', roles]))
3 changes: 3 additions & 0 deletions docs/user_guide/getting_started/minimal_script.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ Summary: Complete Examples
For ease of use, here are a pair of examples.

One of them is exactly the same as the tutorial steps above, in a single block.

The other example includes an explicit login step, so you can control when that
login flow happens!
It also converts the app to be used as a context manager, which will make it
automatically close network connections and the token storage on exit.

*These examples are complete. They should run without errors "as is".*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@
# Confidential Client ID/Secret - <replace these with real client values>
CONFIDENTIAL_CLIENT_ID = "..."
CONFIDENTIAL_CLIENT_SECRET = "..."
CLIENT_APP = ClientApp(
"my-simple-client-collection",
client_id=CONFIDENTIAL_CLIENT_ID,
client_secret=CONFIDENTIAL_CLIENT_SECRET,
)


# Globus Tutorial Collection 1
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
Expand All @@ -19,22 +13,27 @@


def main():
gcs_client = globus_sdk.GCSClient(ENDPOINT_HOSTNAME, app=CLIENT_APP)

# Comment out this line if the mapped collection is high assurance
attach_data_access_scope(gcs_client, MAPPED_COLLECTION_ID)

ensure_user_credential(gcs_client)

collection_request = globus_sdk.GuestCollectionDocument(
public=True,
collection_base_path="/",
display_name="example_guest_collection",
mapped_collection_id=MAPPED_COLLECTION_ID,
)

collection = gcs_client.create_collection(collection_request)
print(f"Created guest collection. Collection ID: {collection['id']}")
with ClientApp(
"my-simple-client-collection",
client_id=CONFIDENTIAL_CLIENT_ID,
client_secret=CONFIDENTIAL_CLIENT_SECRET,
) as app:
gcs_client = globus_sdk.GCSClient(ENDPOINT_HOSTNAME, app=app)

# Comment out this line if the mapped collection is high assurance
attach_data_access_scope(gcs_client, MAPPED_COLLECTION_ID)

ensure_user_credential(gcs_client)

collection_request = globus_sdk.GuestCollectionDocument(
public=True,
collection_base_path="/",
display_name="example_guest_collection",
mapped_collection_id=MAPPED_COLLECTION_ID,
)

collection = gcs_client.create_collection(collection_request)
print(f"Created guest collection. Collection ID: {collection['id']}")


def attach_data_access_scope(gcs_client, collection_id):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ the service:

# Tutorial Client ID - <replace this with your own client>
NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
USER_APP = globus_sdk.UserApp("detect-data-access-example", client_id=NATIVE_CLIENT_ID)

# Globus Tutorial Collection 1
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
# replace with your own COLLECTION_ID
COLLECTION_ID = "6c54cade-bde5-45c1-bdea-f4bd71dba2cc"

transfer_client = globus_sdk.TransferClient(app=USER_APP)

collection_doc = transfer_client.get_endpoint(COLLECTION_ID)
with globus_sdk.UserApp(
"detect-data-access-example", client_id=NATIVE_CLIENT_ID
) as app:
transfer_client = globus_sdk.TransferClient(app=app)
collection_doc = transfer_client.get_endpoint(COLLECTION_ID)

.. note::

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ def uses_data_access(

# Tutorial Client ID - <replace this with your own client>
NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
USER_APP = globus_sdk.UserApp("detect-data-access-example", client_id=NATIVE_CLIENT_ID)


# Globus Tutorial Collection 1 & 2
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
Expand All @@ -33,17 +31,21 @@ def uses_data_access(
SRC_PATH = "/home/share/godata/file1.txt"
DST_PATH = "/~/example-transfer-script-destination.txt"

transfer_client = globus_sdk.TransferClient(app=USER_APP)

# check if either source or dest needs data_access, and if so add the relevant
# requirement
if uses_data_access(transfer_client, SRC_COLLECTION):
transfer_client.add_app_data_access_scope(SRC_COLLECTION)
if uses_data_access(transfer_client, DST_COLLECTION):
transfer_client.add_app_data_access_scope(DST_COLLECTION)
with globus_sdk.UserApp(
"detect-data-access-example", client_id=NATIVE_CLIENT_ID
) as app:
transfer_client = globus_sdk.TransferClient(app=app)

# check if either source or dest needs data_access, and if so add the relevant
# requirement
if uses_data_access(transfer_client, SRC_COLLECTION):
transfer_client.add_app_data_access_scope(SRC_COLLECTION)
if uses_data_access(transfer_client, DST_COLLECTION):
transfer_client.add_app_data_access_scope(DST_COLLECTION)

transfer_request = globus_sdk.TransferData(SRC_COLLECTION, DST_COLLECTION)
transfer_request.add_item(SRC_PATH, DST_PATH)
transfer_request = globus_sdk.TransferData(SRC_COLLECTION, DST_COLLECTION)
transfer_request.add_item(SRC_PATH, DST_PATH)

task = transfer_client.submit_transfer(transfer_request)
print(f"Submitted transfer. Task ID: {task['task_id']}.")
task = transfer_client.submit_transfer(transfer_request)
print(f"Submitted transfer. Task ID: {task['task_id']}.")
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

# Tutorial Client ID - <replace this with your own client>
NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
USER_APP = UserApp("manage-timers-example", client_id=NATIVE_CLIENT_ID)

# Globus Tutorial Collection 1
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
Expand Down Expand Up @@ -36,24 +35,25 @@
},
)

# create a TimersClient to interact with the service, and register any data_access
# scopes for the collections
timers_client = globus_sdk.TimersClient(app=USER_APP)
# Omit this step if the collections are either
# (1) A guest collection or (2) high assurance.
timers_client.add_app_transfer_data_access_scope((SRC_COLLECTION, DST_COLLECTION))

# submit the creation request to the service, printing out the ID of your new timer
# after it's created -- you can find it in https://app.globus.org/activity/timers
timer = timers_client.create_timer(
globus_sdk.TransferTimer(
name=(
"create-timer-example "
f"[created at {datetime.datetime.now().isoformat()}]"
),
body=transfer_request,
schedule=schedule,
with UserApp("manage-timers-example", client_id=NATIVE_CLIENT_ID) as app:
# create a TimersClient to interact with the service, and register any data_access
# scopes for the collections
timers_client = globus_sdk.TimersClient(app=app)
# Omit this step if the collections are either
# (1) A guest collection or (2) high assurance.
timers_client.add_app_transfer_data_access_scope((SRC_COLLECTION, DST_COLLECTION))

# submit the creation request to the service, printing out the ID of your new timer
# after it's created -- you can find it in https://app.globus.org/activity/timers
timer = timers_client.create_timer(
globus_sdk.TransferTimer(
name=(
"create-timer-example "
f"[created at {datetime.datetime.now().isoformat()}]"
),
body=transfer_request,
schedule=schedule,
)
)
)
print("Finished submitting timer.")
print(f"timer_id: {timer['timer']['job_id']}")
print("Finished submitting timer.")
print(f"timer_id: {timer['timer']['job_id']}")
Loading