Skip to content

Commit cd76dbd

Browse files
committed
Add support for Globus Tunnels
This patch adds methods to the TransferClient that will allow for interaction with the Globus Streams functionality.
1 parent f1f073a commit cd76dbd

File tree

18 files changed

+738
-3
lines changed

18 files changed

+738
-3
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Added
2+
-----
3+
4+
- Added support to the ``TransferClient`` for the Streams API (:pr:`NUMBER`)
5+
- ``CreateTunnelData`` is a payload builder for tunnel creation documents
6+
- ``TransferClient.create_tunnel()`` supports tunnel creation
7+
- ``TransferClient.update_tunnel()`` supports updates to a tunnel
8+
- ``TransferClient.get_tunnel()`` fetches a tunnel by ID
9+
- ``TransferClient.delete_tunnel()`` deletes a tunnel
10+
- ``TransferClient.list_tunnels()`` fetches all of the current user's tunnels
11+
- ``TransferClient.get_stream_access_point()`` fetches a Stream Access Point by ID

src/globus_sdk/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ from .services.timers import (
116116
TransferTimer,
117117
)
118118
from .services.transfer import (
119+
CreateTunnelData,
119120
DeleteData,
120121
IterableTransferResponse,
121122
TransferAPIError,

src/globus_sdk/services/transfer/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .client import TransferClient
2-
from .data import DeleteData, TransferData
2+
from .data import CreateTunnelData, DeleteData, TransferData
33
from .errors import TransferAPIError
44
from .response import IterableTransferResponse
55

@@ -9,4 +9,5 @@
99
"DeleteData",
1010
"TransferAPIError",
1111
"IterableTransferResponse",
12+
"CreateTunnelData",
1213
)

src/globus_sdk/services/transfer/client.py

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from globus_sdk.scopes import GCSCollectionScopes, Scope, TransferScopes
1414
from globus_sdk.transport import RetryConfig
1515

16-
from .data import DeleteData, TransferData
16+
from .data import CreateTunnelData, DeleteData, TransferData
1717
from .errors import TransferAPIError
1818
from .response import IterableTransferResponse
1919
from .transport import TRANSFER_DEFAULT_RETRY_CHECKS
@@ -2699,3 +2699,195 @@ def endpoint_manager_delete_pause_rule(
26992699
f"/v0.10/endpoint_manager/pause_rule/{pause_rule_id}",
27002700
query_params=query_params,
27012701
)
2702+
2703+
# Tunnel methods
2704+
2705+
def create_tunnel(
2706+
self,
2707+
data: dict[str, t.Any] | CreateTunnelData,
2708+
) -> response.GlobusHTTPResponse:
2709+
"""
2710+
:param data: Parameters for the tunnel creation
2711+
2712+
.. tab-set::
2713+
2714+
.. tab-item:: Example Usage
2715+
2716+
.. code-block:: python
2717+
2718+
tc = globus_sdk.TunnelClient(...)
2719+
result = tc.create_tunnel(data)
2720+
print(result["data"]["id"])
2721+
2722+
.. tab-item:: API Info
2723+
2724+
``POST /v2/tunnels``
2725+
"""
2726+
log.debug("TransferClient.create_tunnel(...)")
2727+
try:
2728+
data_element = data["data"]
2729+
except KeyError:
2730+
raise exc.GlobusSDKUsageError(
2731+
"create_tunnel() body was malformed (missing the 'data' key). "
2732+
"Use CreateTunnelData to easily create correct documents."
2733+
)
2734+
2735+
try:
2736+
attributes = data_element["attributes"]
2737+
except KeyError:
2738+
data_element["attributes"] = {}
2739+
attributes = data_element["attributes"]
2740+
if attributes.get("submission_id", MISSING) is MISSING:
2741+
log.debug("create_tunnel auto-fetching submission_id")
2742+
attributes["submission_id"] = self.get_submission_id()["value"]
2743+
2744+
r = self.post("/v2/tunnels", data=data)
2745+
return r
2746+
2747+
def update_tunnel(
2748+
self,
2749+
tunnel_id: str,
2750+
update_doc: dict[str, t.Any],
2751+
) -> response.GlobusHTTPResponse:
2752+
r"""
2753+
:param tunnel_id: The ID of the Tunnel.
2754+
:param update_doc: The document that will be sent to the patch API.
2755+
2756+
.. tab-set::
2757+
2758+
.. tab-item:: Example Usage
2759+
2760+
.. code-block:: python
2761+
2762+
tc = globus_sdk.TunnelClient(...)
2763+
"data" = {
2764+
"type": "Tunnel",
2765+
"attributes": {
2766+
"state": "STOPPING",
2767+
},
2768+
}
2769+
result = tc.update_tunnel(tunnel_id, data)
2770+
print(result["data"])
2771+
2772+
.. tab-item:: API Info
2773+
2774+
``PATCH /v2/tunnels/<tunnel_id>``
2775+
"""
2776+
r = self.patch(f"/v2/tunnels/{tunnel_id}", data=update_doc)
2777+
return r
2778+
2779+
def get_tunnel(
2780+
self,
2781+
tunnel_id: str,
2782+
*,
2783+
query_params: dict[str, t.Any] | None = None,
2784+
) -> response.GlobusHTTPResponse:
2785+
"""
2786+
:param tunnel_id: The ID of the Tunnel which we are fetching details about.
2787+
:param query_params: Any additional parameters will be passed through
2788+
as query params.
2789+
2790+
.. tab-set::
2791+
2792+
.. tab-item:: Example Usage
2793+
2794+
.. code-block:: python
2795+
2796+
tc = globus_sdk.TunnelClient(...)
2797+
result = tc.show_tunnel(tunnel_id)
2798+
print(result["data"])
2799+
2800+
.. tab-item:: API Info
2801+
2802+
``GET /v2/tunnels/<tunnel_id>``
2803+
"""
2804+
log.debug("TransferClient.get_tunnel(...)")
2805+
r = self.get(f"/v2/tunnels/{tunnel_id}", query_params=query_params)
2806+
return r
2807+
2808+
def delete_tunnel(
2809+
self,
2810+
tunnel_id: str,
2811+
) -> response.GlobusHTTPResponse:
2812+
"""
2813+
:param tunnel_id: The ID of the Tunnel to be deleted.
2814+
2815+
This will clean up all data associated with a Tunnel.
2816+
Note that Tunnels must be stopped before they can be deleted.
2817+
2818+
.. tab-set::
2819+
2820+
.. tab-item:: Example Usage
2821+
2822+
.. code-block:: python
2823+
2824+
tc = globus_sdk.TunnelClient(...)
2825+
tc.delete_tunnel(tunnel_id)
2826+
2827+
.. tab-item:: API Info
2828+
2829+
``DELETE /v2/tunnels/<tunnel_id>``
2830+
"""
2831+
log.debug("TransferClient.delete_tunnel(...)")
2832+
r = self.delete(f"/v2/tunnels/{tunnel_id}")
2833+
return r
2834+
2835+
def list_tunnels(
2836+
self,
2837+
*,
2838+
query_params: dict[str, t.Any] | None = None,
2839+
) -> IterableTransferResponse:
2840+
"""
2841+
:param query_params: Any additional parameters will be passed through
2842+
as query params.
2843+
2844+
This will list all the Tunnels created by the authorized user.
2845+
2846+
.. tab-set::
2847+
2848+
.. tab-item:: Example Usage
2849+
2850+
.. code-block:: python
2851+
2852+
tc = globus_sdk.TunnelClient(...)
2853+
tc.list_tunnels(tunnel_id)
2854+
2855+
.. tab-item:: API Info
2856+
2857+
``GET /v2/tunnels/``
2858+
"""
2859+
log.debug("TransferClient.list_tunnels(...)")
2860+
r = self.get("/v2/tunnels", query_params=query_params)
2861+
return IterableTransferResponse(r)
2862+
2863+
def get_stream_access_point(
2864+
self,
2865+
stream_ap_id: str,
2866+
*,
2867+
query_params: dict[str, t.Any] | None = None,
2868+
) -> response.GlobusHTTPResponse:
2869+
"""
2870+
:param stream_ap_id: The ID of the steaming access point to lookup.
2871+
:param query_params: Any additional parameters will be passed through
2872+
as query params.
2873+
2874+
This will list all the Tunnels created by the authorized user.
2875+
2876+
.. tab-set::
2877+
2878+
.. tab-item:: Example Usage
2879+
2880+
.. code-block:: python
2881+
2882+
tc = globus_sdk.TunnelClient(...)
2883+
tc.get_stream_ap(stream_ap_id)
2884+
2885+
.. tab-item:: API Info
2886+
2887+
``GET /v2/stream_access_points/<stream_ap_id>``
2888+
"""
2889+
log.debug("TransferClient.get_stream_ap(...)")
2890+
r = self.get(
2891+
f"/v2/stream_access_points/{stream_ap_id}", query_params=query_params
2892+
)
2893+
return r

src/globus_sdk/services/transfer/data/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66

77
from .delete_data import DeleteData
88
from .transfer_data import TransferData
9+
from .tunnel_data import CreateTunnelData
910

10-
__all__ = ("TransferData", "DeleteData")
11+
__all__ = ("TransferData", "DeleteData", "CreateTunnelData")
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import typing as t
5+
import uuid
6+
7+
from globus_sdk._missing import MISSING, MissingType
8+
from globus_sdk._payload import GlobusPayload
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
class CreateTunnelData(GlobusPayload):
14+
def __init__(
15+
self,
16+
initiator_stream_access_point: uuid.UUID | str,
17+
listener_stream_access_point: uuid.UUID | str,
18+
*,
19+
label: str | MissingType = MISSING,
20+
submission_id: uuid.UUID | str | MissingType = MISSING,
21+
lifetime_mins: int | MissingType = MISSING,
22+
restartable: bool | MissingType = MISSING,
23+
additional_fields: dict[str, t.Any] | None = None,
24+
) -> None:
25+
super().__init__()
26+
log.debug("Creating a new TunnelData object")
27+
28+
relationships = {
29+
"listener": {
30+
"data": {
31+
"type": "StreamAccessPoint",
32+
"id": listener_stream_access_point,
33+
}
34+
},
35+
"initiator": {
36+
"data": {
37+
"type": "StreamAccessPoint",
38+
"id": initiator_stream_access_point,
39+
}
40+
},
41+
}
42+
attributes = {
43+
"label": label,
44+
"submission_id": submission_id,
45+
"restartable": restartable,
46+
"lifetime_mins": lifetime_mins,
47+
}
48+
if additional_fields is not None:
49+
attributes.update(additional_fields)
50+
51+
self["data"] = {
52+
"type": "Tunnel",
53+
"relationships": relationships,
54+
"attributes": attributes,
55+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import uuid
2+
3+
from globus_sdk.testing.models import RegisteredResponse, ResponseSet
4+
5+
TUNNEL_ID = str(uuid.uuid4())
6+
7+
_initiator_ap = str(uuid.uuid4())
8+
_listener_ap = str(uuid.uuid4())
9+
10+
_default_display_name = "Test Tunnel"
11+
12+
13+
RESPONSES = ResponseSet(
14+
default=RegisteredResponse(
15+
service="transfer",
16+
method="POST",
17+
path="/v2/tunnels",
18+
json={
19+
"data": {
20+
"attributes": {
21+
"created_time": "2025-12-12T21:49:22.183977",
22+
"initiator_ip_address": None,
23+
"initiator_port": None,
24+
"label": _default_display_name,
25+
"lifetime_mins": 10,
26+
"listener_ip_address": None,
27+
"listener_port": None,
28+
"restartable": False,
29+
"state": "AWAITING_LISTENER",
30+
"status": "The tunnel is waiting for listening.",
31+
"submission_id": "6ab42cda-d7a4-11f0-ad34-0affc202d2e9",
32+
},
33+
"id": "34d97133-f17e-4f90-ad42-56ff5f3c2550",
34+
"relationships": {
35+
"initiator": {
36+
"data": {"id": _initiator_ap, "type": "StreamAccessPoint"}
37+
},
38+
"listener": {
39+
"data": {"id": _listener_ap, "type": "StreamAccessPoint"}
40+
},
41+
"owner": {
42+
"data": {
43+
"id": "4d443580-012d-4954-816f-e0592bd356e1",
44+
"type": "Identity",
45+
}
46+
},
47+
},
48+
"type": "Tunnel",
49+
},
50+
"meta": {"request_id": "e6KkKkNmw"},
51+
},
52+
metadata={
53+
"tunnel_id": TUNNEL_ID,
54+
"display_name": _default_display_name,
55+
"initiator_ap": _initiator_ap,
56+
"listener_ap": _listener_ap,
57+
},
58+
),
59+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import uuid
2+
3+
from globus_sdk.testing.models import RegisteredResponse, ResponseSet
4+
5+
TUNNEL_ID = str(uuid.uuid4())
6+
7+
8+
RESPONSES = ResponseSet(
9+
default=RegisteredResponse(
10+
service="transfer",
11+
method="DELETE",
12+
path=f"/v2/tunnels/{TUNNEL_ID}",
13+
json={"data": None, "meta": {"request_id": "ofayi2B4R"}},
14+
metadata={
15+
"tunnel_id": TUNNEL_ID,
16+
},
17+
),
18+
)

0 commit comments

Comments
 (0)