From 7154745f726d27c9253e1c97baccda136c5fa1ce Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 4 Dec 2025 09:56:53 -0600 Subject: [PATCH] Add support for Globus Tunnels This patch adds methods to the TransferClient that will allow for interaction with the Globus Streams functionality. --- changelog.d/20251205_104316_john_tunnels.rst | 12 ++ src/globus_sdk/__init__.pyi | 1 + src/globus_sdk/client.py | 2 +- src/globus_sdk/services/transfer/__init__.py | 3 +- src/globus_sdk/services/transfer/client.py | 194 +++++++++++++++++- .../services/transfer/data/__init__.py | 3 +- .../services/transfer/data/tunnel_data.py | 55 +++++ .../testing/data/transfer/create_tunnel.py | 59 ++++++ .../testing/data/transfer/delete_tunnel.py | 18 ++ .../data/transfer/get_stream_access_point.py | 47 +++++ .../testing/data/transfer/get_tunnel.py | 56 +++++ .../testing/data/transfer/list_tunnels.py | 93 +++++++++ .../testing/data/transfer/update_tunnel.py | 57 +++++ .../services/transfer/test_create_tunnel.py | 71 +++++++ .../services/transfer/test_delete_tunnel.py | 11 + .../transfer/test_get_stream_access_point.py | 13 ++ .../services/transfer/test_get_tunnel.py | 13 ++ .../services/transfer/test_list_tunnel.py | 12 ++ .../services/transfer/test_update_tunnel.py | 24 +++ 19 files changed, 740 insertions(+), 4 deletions(-) create mode 100644 changelog.d/20251205_104316_john_tunnels.rst create mode 100644 src/globus_sdk/services/transfer/data/tunnel_data.py create mode 100644 src/globus_sdk/testing/data/transfer/create_tunnel.py create mode 100644 src/globus_sdk/testing/data/transfer/delete_tunnel.py create mode 100644 src/globus_sdk/testing/data/transfer/get_stream_access_point.py create mode 100644 src/globus_sdk/testing/data/transfer/get_tunnel.py create mode 100644 src/globus_sdk/testing/data/transfer/list_tunnels.py create mode 100644 src/globus_sdk/testing/data/transfer/update_tunnel.py create mode 100644 tests/functional/services/transfer/test_create_tunnel.py create mode 100644 tests/functional/services/transfer/test_delete_tunnel.py create mode 100644 tests/functional/services/transfer/test_get_stream_access_point.py create mode 100644 tests/functional/services/transfer/test_get_tunnel.py create mode 100644 tests/functional/services/transfer/test_list_tunnel.py create mode 100644 tests/functional/services/transfer/test_update_tunnel.py diff --git a/changelog.d/20251205_104316_john_tunnels.rst b/changelog.d/20251205_104316_john_tunnels.rst new file mode 100644 index 000000000..d6ee2ceaf --- /dev/null +++ b/changelog.d/20251205_104316_john_tunnels.rst @@ -0,0 +1,12 @@ +Added +----- + +- Added support to the ``TransferClient`` for the Streams API (:pr:`NUMBER`) + + - ``CreateTunnelData`` is a payload builder for tunnel creation documents + - ``TransferClient.create_tunnel()`` supports tunnel creation + - ``TransferClient.update_tunnel()`` supports updates to a tunnel + - ``TransferClient.get_tunnel()`` fetches a tunnel by ID + - ``TransferClient.delete_tunnel()`` deletes a tunnel + - ``TransferClient.list_tunnels()`` fetches all of the current user's tunnels + - ``TransferClient.get_stream_access_point()`` fetches a Stream Access Point by ID diff --git a/src/globus_sdk/__init__.pyi b/src/globus_sdk/__init__.pyi index 883e1b17a..53cf71669 100644 --- a/src/globus_sdk/__init__.pyi +++ b/src/globus_sdk/__init__.pyi @@ -116,6 +116,7 @@ from .services.timers import ( TransferTimer, ) from .services.transfer import ( + CreateTunnelData, DeleteData, IterableTransferResponse, TransferAPIError, diff --git a/src/globus_sdk/client.py b/src/globus_sdk/client.py index c7b5138fc..fec238f6c 100644 --- a/src/globus_sdk/client.py +++ b/src/globus_sdk/client.py @@ -540,7 +540,7 @@ def request( # if a client is asked to make a request against a full URL, not just the path # component, then do not resolve the path, simply pass it through as the URL - if path.startswith("https://") or path.startswith("http://"): + if path.startswith(("https://", "http://")): url = path else: url = slash_join(self.base_url, urllib.parse.quote(path)) diff --git a/src/globus_sdk/services/transfer/__init__.py b/src/globus_sdk/services/transfer/__init__.py index 2fa33d11a..bebae655b 100644 --- a/src/globus_sdk/services/transfer/__init__.py +++ b/src/globus_sdk/services/transfer/__init__.py @@ -1,5 +1,5 @@ from .client import TransferClient -from .data import DeleteData, TransferData +from .data import CreateTunnelData, DeleteData, TransferData from .errors import TransferAPIError from .response import IterableTransferResponse @@ -9,4 +9,5 @@ "DeleteData", "TransferAPIError", "IterableTransferResponse", + "CreateTunnelData", ) diff --git a/src/globus_sdk/services/transfer/client.py b/src/globus_sdk/services/transfer/client.py index eb77be85c..1de209b78 100644 --- a/src/globus_sdk/services/transfer/client.py +++ b/src/globus_sdk/services/transfer/client.py @@ -13,7 +13,7 @@ from globus_sdk.scopes import GCSCollectionScopes, Scope, TransferScopes from globus_sdk.transport import RetryConfig -from .data import DeleteData, TransferData +from .data import CreateTunnelData, DeleteData, TransferData from .errors import TransferAPIError from .response import IterableTransferResponse from .transport import TRANSFER_DEFAULT_RETRY_CHECKS @@ -2699,3 +2699,195 @@ def endpoint_manager_delete_pause_rule( f"/v0.10/endpoint_manager/pause_rule/{pause_rule_id}", query_params=query_params, ) + + # Tunnel methods + + def create_tunnel( + self, + data: dict[str, t.Any] | CreateTunnelData, + ) -> response.GlobusHTTPResponse: + """ + :param data: Parameters for the tunnel creation + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: python + + tc = globus_sdk.TunnelClient(...) + result = tc.create_tunnel(data) + print(result["data"]["id"]) + + .. tab-item:: API Info + + ``POST /v2/tunnels`` + """ + log.debug("TransferClient.create_tunnel(...)") + try: + data_element = data["data"] + except KeyError as e: + raise exc.GlobusSDKUsageError( + "create_tunnel() body was malformed (missing the 'data' key). " + "Use CreateTunnelData to easily create correct documents." + ) from e + + try: + attributes = data_element["attributes"] + except KeyError: + data_element["attributes"] = {} + attributes = data_element["attributes"] + if attributes.get("submission_id", MISSING) is MISSING: + log.debug("create_tunnel auto-fetching submission_id") + attributes["submission_id"] = self.get_submission_id()["value"] + + r = self.post("/v2/tunnels", data=data) + return r + + def update_tunnel( + self, + tunnel_id: str, + update_doc: dict[str, t.Any], + ) -> response.GlobusHTTPResponse: + r""" + :param tunnel_id: The ID of the Tunnel. + :param update_doc: The document that will be sent to the patch API. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: python + + tc = globus_sdk.TunnelClient(...) + "data" = { + "type": "Tunnel", + "attributes": { + "state": "STOPPING", + }, + } + result = tc.update_tunnel(tunnel_id, data) + print(result["data"]) + + .. tab-item:: API Info + + ``PATCH /v2/tunnels/`` + """ + r = self.patch(f"/v2/tunnels/{tunnel_id}", data=update_doc) + return r + + def get_tunnel( + self, + tunnel_id: str, + *, + query_params: dict[str, t.Any] | None = None, + ) -> response.GlobusHTTPResponse: + """ + :param tunnel_id: The ID of the Tunnel which we are fetching details about. + :param query_params: Any additional parameters will be passed through + as query params. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: python + + tc = globus_sdk.TunnelClient(...) + result = tc.show_tunnel(tunnel_id) + print(result["data"]) + + .. tab-item:: API Info + + ``GET /v2/tunnels/`` + """ + log.debug("TransferClient.get_tunnel(...)") + r = self.get(f"/v2/tunnels/{tunnel_id}", query_params=query_params) + return r + + def delete_tunnel( + self, + tunnel_id: str, + ) -> response.GlobusHTTPResponse: + """ + :param tunnel_id: The ID of the Tunnel to be deleted. + + This will clean up all data associated with a Tunnel. + Note that Tunnels must be stopped before they can be deleted. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: python + + tc = globus_sdk.TunnelClient(...) + tc.delete_tunnel(tunnel_id) + + .. tab-item:: API Info + + ``DELETE /v2/tunnels/`` + """ + log.debug("TransferClient.delete_tunnel(...)") + r = self.delete(f"/v2/tunnels/{tunnel_id}") + return r + + def list_tunnels( + self, + *, + query_params: dict[str, t.Any] | None = None, + ) -> IterableTransferResponse: + """ + :param query_params: Any additional parameters will be passed through + as query params. + + This will list all the Tunnels created by the authorized user. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: python + + tc = globus_sdk.TunnelClient(...) + tc.list_tunnels(tunnel_id) + + .. tab-item:: API Info + + ``GET /v2/tunnels/`` + """ + log.debug("TransferClient.list_tunnels(...)") + r = self.get("/v2/tunnels", query_params=query_params) + return IterableTransferResponse(r) + + def get_stream_access_point( + self, + stream_ap_id: str, + *, + query_params: dict[str, t.Any] | None = None, + ) -> response.GlobusHTTPResponse: + """ + :param stream_ap_id: The ID of the steaming access point to lookup. + :param query_params: Any additional parameters will be passed through + as query params. + + This will list all the Tunnels created by the authorized user. + + .. tab-set:: + + .. tab-item:: Example Usage + + .. code-block:: python + + tc = globus_sdk.TunnelClient(...) + tc.get_stream_ap(stream_ap_id) + + .. tab-item:: API Info + + ``GET /v2/stream_access_points/`` + """ + log.debug("TransferClient.get_stream_ap(...)") + r = self.get( + f"/v2/stream_access_points/{stream_ap_id}", query_params=query_params + ) + return r diff --git a/src/globus_sdk/services/transfer/data/__init__.py b/src/globus_sdk/services/transfer/data/__init__.py index b5904588a..26e7ce2b7 100644 --- a/src/globus_sdk/services/transfer/data/__init__.py +++ b/src/globus_sdk/services/transfer/data/__init__.py @@ -6,5 +6,6 @@ from .delete_data import DeleteData from .transfer_data import TransferData +from .tunnel_data import CreateTunnelData -__all__ = ("TransferData", "DeleteData") +__all__ = ("TransferData", "DeleteData", "CreateTunnelData") diff --git a/src/globus_sdk/services/transfer/data/tunnel_data.py b/src/globus_sdk/services/transfer/data/tunnel_data.py new file mode 100644 index 000000000..0098a76fc --- /dev/null +++ b/src/globus_sdk/services/transfer/data/tunnel_data.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import logging +import typing as t +import uuid + +from globus_sdk._missing import MISSING, MissingType +from globus_sdk._payload import GlobusPayload + +log = logging.getLogger(__name__) + + +class CreateTunnelData(GlobusPayload): + def __init__( + self, + initiator_stream_access_point: uuid.UUID | str, + listener_stream_access_point: uuid.UUID | str, + *, + label: str | MissingType = MISSING, + submission_id: uuid.UUID | str | MissingType = MISSING, + lifetime_mins: int | MissingType = MISSING, + restartable: bool | MissingType = MISSING, + additional_fields: dict[str, t.Any] | None = None, + ) -> None: + super().__init__() + log.debug("Creating a new TunnelData object") + + relationships = { + "listener": { + "data": { + "type": "StreamAccessPoint", + "id": listener_stream_access_point, + } + }, + "initiator": { + "data": { + "type": "StreamAccessPoint", + "id": initiator_stream_access_point, + } + }, + } + attributes = { + "label": label, + "submission_id": submission_id, + "restartable": restartable, + "lifetime_mins": lifetime_mins, + } + if additional_fields is not None: + attributes.update(additional_fields) + + self["data"] = { + "type": "Tunnel", + "relationships": relationships, + "attributes": attributes, + } diff --git a/src/globus_sdk/testing/data/transfer/create_tunnel.py b/src/globus_sdk/testing/data/transfer/create_tunnel.py new file mode 100644 index 000000000..cc0b80f48 --- /dev/null +++ b/src/globus_sdk/testing/data/transfer/create_tunnel.py @@ -0,0 +1,59 @@ +import uuid + +from globus_sdk.testing.models import RegisteredResponse, ResponseSet + +TUNNEL_ID = str(uuid.uuid4()) + +_initiator_ap = str(uuid.uuid4()) +_listener_ap = str(uuid.uuid4()) + +_default_display_name = "Test Tunnel" + + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="transfer", + method="POST", + path="/v2/tunnels", + json={ + "data": { + "attributes": { + "created_time": "2025-12-12T21:49:22.183977", + "initiator_ip_address": None, + "initiator_port": None, + "label": _default_display_name, + "lifetime_mins": 10, + "listener_ip_address": None, + "listener_port": None, + "restartable": False, + "state": "AWAITING_LISTENER", + "status": "The tunnel is waiting for listening.", + "submission_id": "6ab42cda-d7a4-11f0-ad34-0affc202d2e9", + }, + "id": "34d97133-f17e-4f90-ad42-56ff5f3c2550", + "relationships": { + "initiator": { + "data": {"id": _initiator_ap, "type": "StreamAccessPoint"} + }, + "listener": { + "data": {"id": _listener_ap, "type": "StreamAccessPoint"} + }, + "owner": { + "data": { + "id": "4d443580-012d-4954-816f-e0592bd356e1", + "type": "Identity", + } + }, + }, + "type": "Tunnel", + }, + "meta": {"request_id": "e6KkKkNmw"}, + }, + metadata={ + "tunnel_id": TUNNEL_ID, + "display_name": _default_display_name, + "initiator_ap": _initiator_ap, + "listener_ap": _listener_ap, + }, + ), +) diff --git a/src/globus_sdk/testing/data/transfer/delete_tunnel.py b/src/globus_sdk/testing/data/transfer/delete_tunnel.py new file mode 100644 index 000000000..f06802cf9 --- /dev/null +++ b/src/globus_sdk/testing/data/transfer/delete_tunnel.py @@ -0,0 +1,18 @@ +import uuid + +from globus_sdk.testing.models import RegisteredResponse, ResponseSet + +TUNNEL_ID = str(uuid.uuid4()) + + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="transfer", + method="DELETE", + path=f"/v2/tunnels/{TUNNEL_ID}", + json={"data": None, "meta": {"request_id": "ofayi2B4R"}}, + metadata={ + "tunnel_id": TUNNEL_ID, + }, + ), +) diff --git a/src/globus_sdk/testing/data/transfer/get_stream_access_point.py b/src/globus_sdk/testing/data/transfer/get_stream_access_point.py new file mode 100644 index 000000000..bc69e9ce4 --- /dev/null +++ b/src/globus_sdk/testing/data/transfer/get_stream_access_point.py @@ -0,0 +1,47 @@ +import uuid + +from globus_sdk.testing.models import RegisteredResponse, ResponseSet + +ACCESS_POINT_ID = str(uuid.uuid4()) + + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="transfer", + method="GET", + path=f"/v2/stream_access_points/{ACCESS_POINT_ID}", + json={ + "data": { + "attributes": { + "advertised_owner": "john@globus.org", + "contact_email": None, + "contact_info": None, + "department": None, + "description": None, + "display_name": "Buzz Dev Listener", + "info_link": None, + "keywords": None, + "organization": None, + "tlsftp_server": ( + "tlsftp://s-463c7.e7d5e.8540." + "test3.zones.dnsteam.globuscs.info:443" + ), + }, + "id": ACCESS_POINT_ID, + "relationships": { + "host_endpoint": { + "data": { + "id": "d6428474-c308-4a2d-8a86-d377915d978b", + "type": "Endpoint", + } + } + }, + "type": "StreamAccessPoint", + }, + "meta": {"request_id": "55QRq2iBa"}, + }, + metadata={ + "access_point_id": ACCESS_POINT_ID, + }, + ), +) diff --git a/src/globus_sdk/testing/data/transfer/get_tunnel.py b/src/globus_sdk/testing/data/transfer/get_tunnel.py new file mode 100644 index 000000000..10af0bdbf --- /dev/null +++ b/src/globus_sdk/testing/data/transfer/get_tunnel.py @@ -0,0 +1,56 @@ +import uuid + +from globus_sdk.testing.models import RegisteredResponse, ResponseSet + +TUNNEL_ID = str(uuid.uuid4()) + +_initiator_ap = str(uuid.uuid4()) +_listener_ap = str(uuid.uuid4()) + + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="transfer", + method="GET", + path=f"/v2/tunnels/{TUNNEL_ID}", + json={ + "data": { + "attributes": { + "created_time": "2025-12-12T21:11:50.525278", + "initiator_ip_address": None, + "initiator_port": None, + "label": "Buzz Tester", + "lifetime_mins": 360, + "listener_ip_address": None, + "listener_port": None, + "restartable": False, + "state": "AWAITING_LISTENER", + "status": "The tunnel is waiting for listening", + "submission_id": "292b0054-7084-46eb-83d6-7a6821b1f77e", + }, + "id": TUNNEL_ID, + "relationships": { + "initiator": { + "data": {"id": _initiator_ap, "type": "StreamAccessPoint"} + }, + "listener": { + "data": {"id": _listener_ap, "type": "StreamAccessPoint"} + }, + "owner": { + "data": { + "id": "4d443580-012d-4954-816f-e0592bd356e1", + "type": "Identity", + } + }, + }, + "type": "Tunnel", + }, + "meta": {"request_id": "M6kFaS949"}, + }, + metadata={ + "tunnel_id": TUNNEL_ID, + "initiator_ap": _initiator_ap, + "listener_ap": _listener_ap, + }, + ), +) diff --git a/src/globus_sdk/testing/data/transfer/list_tunnels.py b/src/globus_sdk/testing/data/transfer/list_tunnels.py new file mode 100644 index 000000000..4aacbc902 --- /dev/null +++ b/src/globus_sdk/testing/data/transfer/list_tunnels.py @@ -0,0 +1,93 @@ +from globus_sdk.testing.models import RegisteredResponse, ResponseSet + +TUNNEL_LIST_DOC = { + "data": [ + { + "attributes": { + "created_time": "2025-12-12T21:11:50.525278", + "initiator_ip_address": None, + "initiator_port": None, + "label": "Buzz Tester", + "lifetime_mins": 360, + "listener_ip_address": None, + "listener_port": None, + "restartable": False, + "state": "AWAITING_LISTENER", + "status": "The tunnel is waiting for listening contact detail setup.", + "submission_id": "292b0054-7084-46eb-83d6-7a6821b1f77e", + }, + "id": "1c1be52d-2d4d-4200-b4ad-d75d43eb0d9c", + "relationships": { + "initiator": { + "data": { + "id": "80583f05-75f3-4825-b8a5-6c3edf0bbc5c", + "type": "StreamAccessPoint", + } + }, + "listener": { + "data": { + "id": "dd5fa993-749f-48fb-86cf-f07ad5797d7e", + "type": "StreamAccessPoint", + } + }, + "owner": { + "data": { + "id": "4d443580-012d-4954-816f-e0592bd356e1", + "type": "Identity", + } + }, + }, + "type": "Tunnel", + }, + { + "attributes": { + "created_time": "2025-12-12T21:22:11.018233", + "initiator_ip_address": None, + "initiator_port": None, + "label": "part 2", + "lifetime_mins": 360, + "listener_ip_address": None, + "listener_port": None, + "restartable": False, + "state": "AWAITING_LISTENER", + "status": "The tunnel is waiting for listening contact detail setup.", + "submission_id": "fb3b1220-1d5f-4dcf-92f5-e7056a514319", + }, + "id": "bf1b0d16-7d93-44eb-8773-9066a750c13e", + "relationships": { + "initiator": { + "data": { + "id": "34c6e671-c011-4bf8-bc30-5ccebada8f3b", + "type": "StreamAccessPoint", + } + }, + "listener": { + "data": { + "id": "dd5fa993-749f-48fb-86cf-f07ad5797d7e", + "type": "StreamAccessPoint", + } + }, + "owner": { + "data": { + "id": "4d443580-012d-4954-816f-e0592bd356e1", + "type": "Identity", + } + }, + }, + "type": "Tunnel", + }, + ], + "links": None, + "meta": {"request_id": "fAAfpnino"}, +} + + +RESPONSES = ResponseSet( + metadata={}, + default=RegisteredResponse( + service="transfer", + path="/v2/tunnels", + json=TUNNEL_LIST_DOC, + method="GET", + ), +) diff --git a/src/globus_sdk/testing/data/transfer/update_tunnel.py b/src/globus_sdk/testing/data/transfer/update_tunnel.py new file mode 100644 index 000000000..809944cc1 --- /dev/null +++ b/src/globus_sdk/testing/data/transfer/update_tunnel.py @@ -0,0 +1,57 @@ +import uuid + +from globus_sdk.testing.models import RegisteredResponse, ResponseSet + +TUNNEL_ID = str(uuid.uuid4()) + + +RESPONSES = ResponseSet( + default=RegisteredResponse( + service="transfer", + method="PATCH", + path=f"/v2/tunnels/{TUNNEL_ID}", + json={ + "data": { + "attributes": { + "created_time": "2025-12-12T21:11:50.525278", + "initiator_ip_address": None, + "initiator_port": None, + "label": "Buzz Tester", + "lifetime_mins": 360, + "listener_ip_address": None, + "listener_port": None, + "restartable": False, + "state": "STOPPING", + "status": "A request to stop tunnel has been received.", + "submission_id": "292b0054-7084-46eb-83d6-7a6821b1f77e", + }, + "id": "1c1be52d-2d4d-4200-b4ad-d75d43eb0d9c", + "relationships": { + "initiator": { + "data": { + "id": "80583f05-75f3-4825-b8a5-6c3edf0bbc5c", + "type": "StreamAccessPoint", + } + }, + "listener": { + "data": { + "id": "dd5fa993-749f-48fb-86cf-f07ad5797d7e", + "type": "StreamAccessPoint", + } + }, + "owner": { + "data": { + "id": "4d443580-012d-4954-816f-e0592bd356e1", + "type": "Identity", + } + }, + }, + "type": "Tunnel", + }, + "meta": {"request_id": "pN0Aact40"}, + }, + metadata={ + "tunnel_id": TUNNEL_ID, + }, + ), +) diff --git a/tests/functional/services/transfer/test_create_tunnel.py b/tests/functional/services/transfer/test_create_tunnel.py new file mode 100644 index 000000000..6ffad8ba0 --- /dev/null +++ b/tests/functional/services/transfer/test_create_tunnel.py @@ -0,0 +1,71 @@ +import json +import uuid + +import pytest + +from globus_sdk import exc +from globus_sdk.services.transfer import CreateTunnelData +from globus_sdk.testing import get_last_request, load_response + + +def test_create_tunnel(client): + meta = load_response(client.create_tunnel).metadata + + submission_id = uuid.uuid4() + data = CreateTunnelData( + meta["initiator_ap"], + meta["listener_ap"], + submission_id=submission_id, + label=meta["display_name"], + ) + + res = client.create_tunnel(data) + assert res.http_status == 200 + assert res["data"]["type"] == "Tunnel" + + req = get_last_request() + sent = json.loads(req.body) + assert ( + sent["data"]["relationships"]["initiator"]["data"]["id"] == meta["initiator_ap"] + ) + assert ( + sent["data"]["relationships"]["listener"]["data"]["id"] == meta["listener_ap"] + ) + assert sent["data"]["attributes"]["submission_id"] == str(submission_id) + assert sent["data"]["attributes"]["label"] == meta["display_name"] + + +def test_create_tunnel_no_submission(client): + meta = load_response(client.create_tunnel).metadata + load_response(client.get_submission_id) + + data = CreateTunnelData( + meta["initiator_ap"], meta["listener_ap"], label=meta["display_name"] + ) + + res = client.create_tunnel(data) + assert res.http_status == 200 + + req = get_last_request() + sent = json.loads(req.body) + assert sent["data"]["attributes"]["submission_id"] is not None + + +def test_create_tunnel_bad_input(client): + data = { + "relationships": { + "listener": { + "data": { + "type": "StreamAccessPoint", + } + }, + "initiator": { + "data": { + "type": "StreamAccessPoint", + } + }, + } + } + + with pytest.raises(exc.GlobusSDKUsageError): + client.create_tunnel(data) diff --git a/tests/functional/services/transfer/test_delete_tunnel.py b/tests/functional/services/transfer/test_delete_tunnel.py new file mode 100644 index 000000000..230e41b32 --- /dev/null +++ b/tests/functional/services/transfer/test_delete_tunnel.py @@ -0,0 +1,11 @@ +from globus_sdk.testing import get_last_request, load_response + + +def test_delete_tunnel(client): + meta = load_response(client.delete_tunnel).metadata + + res = client.delete_tunnel(meta["tunnel_id"]) + assert res.http_status == 200 + + req = get_last_request() + assert req.body is None diff --git a/tests/functional/services/transfer/test_get_stream_access_point.py b/tests/functional/services/transfer/test_get_stream_access_point.py new file mode 100644 index 000000000..39d266b94 --- /dev/null +++ b/tests/functional/services/transfer/test_get_stream_access_point.py @@ -0,0 +1,13 @@ +from globus_sdk.testing import get_last_request, load_response + + +def test_get_tunnel(client): + meta = load_response(client.get_stream_access_point).metadata + + res = client.get_stream_access_point(meta["access_point_id"]) + assert res.http_status == 200 + assert res["data"]["type"] == "StreamAccessPoint" + assert res["data"]["id"] == meta["access_point_id"] + + req = get_last_request() + assert req.body is None diff --git a/tests/functional/services/transfer/test_get_tunnel.py b/tests/functional/services/transfer/test_get_tunnel.py new file mode 100644 index 000000000..a8bea5436 --- /dev/null +++ b/tests/functional/services/transfer/test_get_tunnel.py @@ -0,0 +1,13 @@ +from globus_sdk.testing import get_last_request, load_response + + +def test_get_tunnel(client): + meta = load_response(client.get_tunnel).metadata + + res = client.get_tunnel(meta["tunnel_id"]) + assert res.http_status == 200 + assert res["data"]["type"] == "Tunnel" + assert res["data"]["id"] == meta["tunnel_id"] + + req = get_last_request() + assert req.body is None diff --git a/tests/functional/services/transfer/test_list_tunnel.py b/tests/functional/services/transfer/test_list_tunnel.py new file mode 100644 index 000000000..2418084c6 --- /dev/null +++ b/tests/functional/services/transfer/test_list_tunnel.py @@ -0,0 +1,12 @@ +from globus_sdk.testing import get_last_request, load_response + + +def test_list_tunnel(client): + load_response(client.list_tunnels) + + res = client.list_tunnels() + assert res.http_status == 200 + assert len(res["data"]) == 2 + + req = get_last_request() + assert req.body is None diff --git a/tests/functional/services/transfer/test_update_tunnel.py b/tests/functional/services/transfer/test_update_tunnel.py new file mode 100644 index 000000000..cc8e23684 --- /dev/null +++ b/tests/functional/services/transfer/test_update_tunnel.py @@ -0,0 +1,24 @@ +import json + +from globus_sdk.testing import get_last_request, load_response + + +def test_update_tunnel(client): + meta = load_response(client.update_tunnel).metadata + + label = "New Name" + update_doc = { + "data": { + "type": "Tunnel", + "attributes": { + "label": "New Name", + }, + } + } + res = client.update_tunnel(meta["tunnel_id"], update_doc) + assert res.http_status == 200 + assert res["data"]["type"] == "Tunnel" + + req = get_last_request() + sent = json.loads(req.body) + assert sent["data"]["attributes"]["label"] == label