Skip to content

Commit dae0527

Browse files
authored
Support context manager usage of clients and apps to implicitly close token storage and transports (#1326)
2 parents 5e78524 + 3d3d8df commit dae0527

File tree

26 files changed

+622
-213
lines changed

26 files changed

+622
-213
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Added
2+
-----
3+
4+
- ``GlobusApp`` and SDK client classes now support usage as context managers, and
5+
feature a new ``close()`` method to close internal resources.
6+
``close()`` is automatically called on exit. (:pr:`NUMBER`)
7+
8+
- In support of this, token storages now all feature a ``close()`` method,
9+
which does nothing in the default implementation.
10+
Previously, only storages with underlying resources to manage featured a
11+
``close()`` method.
12+
13+
- ``GlobusApp`` will close any token storage via ``close()`` if the token storage
14+
was created by the app on init. Explicitly created storages will not be closed
15+
and must be explicitly closed via their ``close()`` method.
16+
17+
- Any class inheriting from ``BaseClient`` features ``close()``, which will
18+
close any transport object created during client construction.
19+
20+
- Transports which are created explicitly will not be closed by their clients,
21+
and must be explicitly closed.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed
2+
-----
3+
4+
- Fixed a resource leak in which a ``GlobusApp`` would create internal client
5+
objects and never close the associated transports. (:pr:`NUMBER`)

docs/authorization/globus_app/apps.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,60 @@ The following table provides a comparison of these two options:
8282
a user be the primary data access actor. In these cases, a ``ClientApp``
8383
will be rejected and a ``UserApp`` must be used instead.
8484

85+
86+
Closing Resources via GlobusApps
87+
--------------------------------
88+
89+
When used as context managers, ``GlobusApp``\s automatically call their
90+
``close()`` method on exit.
91+
92+
Closing an app closes the token storage attached to it, unless it was created
93+
explicitly.
94+
This covers any token storage created by the app on init, but not those which
95+
are created and passed in via the config.
96+
97+
For most cases, users are recommended to use the context manager form, and to
98+
allow ``GlobusApp`` to both create and close the token storage.
99+
For example,
100+
101+
.. code-block:: python
102+
103+
from globus_sdk import GlobusAppConfig, UserApp
104+
105+
# create an app configured to create its own sqlite storage
106+
config = GlobusAppConfig(token_storage="sqlite")
107+
with UserApp("sample-app", client_id="FILL_IN_HERE", config=config) as app:
108+
... # any clients, usage, etc.
109+
110+
# after the context manager, any storage is implicitly closed
111+
112+
113+
However, when token storage instances are created by the user, they are not automatically
114+
closed, and the user is responsible for closing them. For example,
115+
in the following usage, the user must close the token storage:
116+
117+
.. code-block:: python
118+
119+
from globus_sdk import GlobusAppConfig, UserApp
120+
from globus_sdk.token_storage import SQLiteTokenStorage
121+
122+
# this token storage is created by the user and will not be closed automatically
123+
sql_storage = SQLiteTokenStorage("tokens.sqlite")
124+
125+
# create an app configured to use this storage
126+
config = GlobusAppConfig(token_storage=sql_storage)
127+
with UserApp("sample-app", client_id="FILL_IN_HERE", config=config) as app:
128+
do_stuff(app)
129+
130+
# a second app uses the same storage, and shares the resource
131+
with UserApp("a-different-app", client_id="OTHER_ID", config=config) as app:
132+
do_stuff(app)
133+
134+
# At this stage, the storage will still be open.
135+
# It should be explicitly closed:
136+
sql_storage.close()
137+
138+
85139
Reference
86140
---------
87141

docs/core/base_client.rst

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,79 @@ requests, encoding data, and handling potential retries. It also may include an
99
optional ``authorizer``, an object responsible for handling token
1010
authentication for requests.
1111

12+
13+
Closing Resources via Clients
14+
-----------------------------
15+
16+
When used as context managers, clients automatically call their ``close()``
17+
method on exit.
18+
19+
Closing a client closes the transport object attached to it if the transport was
20+
created implicitly during init.
21+
This means a transport passed in from the outside will not be closed, but one
22+
which was created by the client will be.
23+
24+
For most cases, users are recommended to use the context manager form, and to
25+
allow clients to both create and close the transport:
26+
27+
.. code-block:: python
28+
29+
from globus_sdk import SearchClient, UserApp
30+
31+
with UserApp("sample-app", client_id="FILL_IN_HERE") as app:
32+
with SearchClient(app=app) as client:
33+
... # any usage
34+
35+
# after the context manager, any transport is implicitly closed
36+
37+
However, if transports are created explicitly, they are not automatically closed,
38+
and the user becomes responsible for closing them.
39+
For example, in the following usage, the user must close the transport:
40+
41+
.. code-block:: python
42+
43+
from globus_sdk import SearchClient, UserApp
44+
from globus_sdk.transport import RequestsTransport
45+
46+
my_transport = RequestsTransport(http_timeout=120.0)
47+
48+
with UserApp("sample-app", client_id="FILL_IN_HERE") as app:
49+
with SearchClient(app=app, transport=my_transport) as client:
50+
... # any usage
51+
52+
# At this stage, the transport will still be open.
53+
# It should be explicitly closed:
54+
my_transport.close()
55+
56+
57+
.. note::
58+
59+
The SDK cannot tell whether or not an explicitly passed transport was bound
60+
to a name before it was passed. Therefore, in usage like the following,
61+
the transport will not automatically be closed:
62+
63+
.. code-block:: python
64+
65+
with SearchClient(app=app, transport=RequestsTransport()) as client:
66+
...
67+
68+
In order to close the transport in such a case, you must explicitly close it.
69+
Since transports are bound to ``client.transport``, the following usage would be a valid
70+
resolution:
71+
72+
.. code-block:: python
73+
74+
with SearchClient(app=app, transport=RequestsTransport()) as client:
75+
...
76+
77+
client.transport.close()
78+
79+
80+
Reference
81+
---------
82+
1283
BaseClient
13-
----------
84+
^^^^^^^^^^
1485

1586
.. autoclass:: globus_sdk.BaseClient
1687
:members: scopes, resource_server, attach_globus_app, get, put, post, patch, delete, request
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import globus_sdk
2+
3+
# this is the tutorial client ID
4+
# replace this string with your ID for production use
5+
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
6+
7+
with globus_sdk.UserApp("my-user-app", client_id=CLIENT_ID) as my_app:
8+
with globus_sdk.GroupsClient(app=my_app) as groups_client:
9+
my_groups = groups_client.get_my_groups()
10+
11+
# print in CSV format
12+
print("ID,Name,Roles")
13+
for group in my_groups:
14+
roles = "|".join({m["role"] for m in group["my_memberships"]})
15+
print(",".join([group["id"], f'"{group["name"]}"', roles]))

docs/user_guide/getting_started/list_groups_with_login.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,15 @@
44
# replace this string with your ID for production use
55
CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
66

7-
# create your app
8-
my_app = globus_sdk.UserApp("my-user-app", client_id=CLIENT_ID)
7+
with globus_sdk.UserApp("my-user-app", client_id=CLIENT_ID) as my_app:
8+
with globus_sdk.GroupsClient(app=my_app) as groups_client:
99

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

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

2017
# print in CSV format
2118
print("ID,Name,Roles")

docs/user_guide/getting_started/minimal_script.rst

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,19 @@ will prompt you to login.
9292
Summary: Complete Examples
9393
--------------------------
9494

95-
For ease of use, here are a pair of examples.
95+
For ease of use, here are a set of examples.
9696

9797
One of them is exactly the same as the tutorial steps above, in a single block.
98-
The other example includes an explicit login step, so you can control when that
98+
99+
The next is a version of the tutorial which leverages the context manager
100+
interfaces of the app and client to do cleanup.
101+
This is slightly more verbose, but such usage is recommended because it ensures
102+
that network and filesystem resources associated with the client and app are
103+
properly closed.
104+
105+
The final example includes an explicit login step, so you can control when that
99106
login flow happens!
107+
Like the previous example, it uses the context manager style to ensure proper cleanup.
100108

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

@@ -108,6 +116,14 @@ login flow happens!
108116
:caption: ``list_groups.py`` [:download:`download <list_groups.py>`]
109117
:language: python
110118

119+
.. tab-item:: With Context Managers
120+
121+
This example is the same as the tutorial, but safely cleans up resources.
122+
123+
.. literalinclude:: list_groups_improved.py
124+
:caption: ``list_groups_improved.py`` [:download:`download <list_groups_improved.py>`]
125+
:language: python
126+
111127
.. tab-item:: Explicit ``login()`` Step
112128

113129
This example is very similar to the tutorial, but uses a separate login

docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_client_owned.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44
# Confidential Client ID/Secret - <replace these with real client values>
55
CONFIDENTIAL_CLIENT_ID = "..."
66
CONFIDENTIAL_CLIENT_SECRET = "..."
7-
CLIENT_APP = ClientApp(
8-
"my-simple-client-collection",
9-
client_id=CONFIDENTIAL_CLIENT_ID,
10-
client_secret=CONFIDENTIAL_CLIENT_SECRET,
11-
)
12-
137

148
# Globus Tutorial Collection 1
159
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
@@ -19,8 +13,16 @@
1913

2014

2115
def main():
22-
gcs_client = globus_sdk.GCSClient(ENDPOINT_HOSTNAME, app=CLIENT_APP)
16+
with ClientApp(
17+
"my-simple-client-collection",
18+
client_id=CONFIDENTIAL_CLIENT_ID,
19+
client_secret=CONFIDENTIAL_CLIENT_SECRET,
20+
) as app:
21+
with globus_sdk.GCSClient(ENDPOINT_HOSTNAME, app=app) as gcs_client:
22+
create_guest_collection(gcs_client)
23+
2324

25+
def create_guest_collection(gcs_client: globus_sdk.GCSClient):
2426
# Comment out this line if the mapped collection is high assurance
2527
attach_data_access_scope(gcs_client, MAPPED_COLLECTION_ID)
2628

docs/user_guide/usage_patterns/data_transfer/create_guest_collection/create_guest_collection_user_owned.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
# Tutorial Client ID - <replace this with your own client>
55
NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
6-
USER_APP = UserApp("my-simple-user-collection", client_id=NATIVE_CLIENT_ID)
76

87
# Globus Tutorial Collection 1
98
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
@@ -13,8 +12,12 @@
1312

1413

1514
def main():
16-
gcs_client = globus_sdk.GCSClient(ENDPOINT_HOSTNAME, app=USER_APP)
15+
with UserApp("my-simple-user-collection", client_id=NATIVE_CLIENT_ID) as app:
16+
with globus_sdk.GCSClient(ENDPOINT_HOSTNAME, app=app) as client:
17+
create_guest_collection(client)
1718

19+
20+
def create_guest_collection(gcs_client: globus_sdk.GCSClient):
1821
# Comment out this line if the mapped collection is high assurance
1922
attach_data_access_scope(gcs_client, MAPPED_COLLECTION_ID)
2023

docs/user_guide/usage_patterns/data_transfer/detecting_data_access/index.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,18 @@ the service:
3636
3737
# Tutorial Client ID - <replace this with your own client>
3838
NATIVE_CLIENT_ID = "61338d24-54d5-408f-a10d-66c06b59f6d2"
39-
USER_APP = globus_sdk.UserApp("detect-data-access-example", client_id=NATIVE_CLIENT_ID)
4039
4140
# Globus Tutorial Collection 1
4241
# https://app.globus.org/file-manager/collections/6c54cade-bde5-45c1-bdea-f4bd71dba2cc
4342
# replace with your own COLLECTION_ID
4443
COLLECTION_ID = "6c54cade-bde5-45c1-bdea-f4bd71dba2cc"
4544
46-
transfer_client = globus_sdk.TransferClient(app=USER_APP)
4745
48-
collection_doc = transfer_client.get_endpoint(COLLECTION_ID)
46+
with globus_sdk.UserApp(
47+
"detect-data-access-example", client_id=NATIVE_CLIENT_ID
48+
) as app:
49+
transfer_client = globus_sdk.TransferClient(app=app)
50+
collection_doc = transfer_client.get_endpoint(COLLECTION_ID)
4951
5052
.. note::
5153

0 commit comments

Comments
 (0)