From 6779ec03ad4437912c2e1c971997d091b9fd9903 Mon Sep 17 00:00:00 2001 From: Reid Mello <30907815+rjmello@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:39:10 -0400 Subject: [PATCH 1/3] Create versioned Compute clients Created `ComputeClientV2` and `ComputeClientV3` classes to support Globus Compute API versions 2 and 3, respectively. The canonical `ComputeClient` is now a subclass of `ComputeClientV2`, preserving backward compatibility. --- docs/services/compute.rst | 30 +++++++++++++++++++ src/globus_sdk/__init__.py | 6 ++++ src/globus_sdk/_generate_init.py | 2 ++ .../_testing/data/compute/v2/__init__.py | 0 .../data/compute/{ => v2}/delete_function.py | 2 +- .../data/compute/{ => v2}/get_function.py | 2 +- .../compute/{ => v2}/register_function.py | 2 +- src/globus_sdk/services/compute/__init__.py | 4 ++- src/globus_sdk/services/compute/client.py | 28 +++++++++++++++-- tests/functional/services/compute/conftest.py | 6 ++-- .../services/compute/test_delete_function.py | 9 ------ .../compute/v2/test_delete_function.py | 9 ++++++ .../compute/{ => v2}/test_get_function.py | 6 ++-- .../{ => v2}/test_register_function.py | 6 ++-- .../compute/test_canononical_client.py | 6 ++++ 15 files changed, 93 insertions(+), 25 deletions(-) create mode 100644 src/globus_sdk/_testing/data/compute/v2/__init__.py rename src/globus_sdk/_testing/data/compute/{ => v2}/delete_function.py (90%) rename src/globus_sdk/_testing/data/compute/{ => v2}/get_function.py (90%) rename src/globus_sdk/_testing/data/compute/{ => v2}/register_function.py (86%) delete mode 100644 tests/functional/services/compute/test_delete_function.py create mode 100644 tests/functional/services/compute/v2/test_delete_function.py rename tests/functional/services/compute/{ => v2}/test_get_function.py (57%) rename tests/functional/services/compute/{ => v2}/test_register_function.py (56%) create mode 100644 tests/unit/services/compute/test_canononical_client.py diff --git a/docs/services/compute.rst b/docs/services/compute.rst index e2cc8588a..b5a586433 100644 --- a/docs/services/compute.rst +++ b/docs/services/compute.rst @@ -3,17 +3,47 @@ Globus Compute .. currentmodule:: globus_sdk +The standard way to interact with the Globus Compute service is through the +`Globus Compute SDK `_, +a separate, specialized toolkit that offers enhanced functionality for Globus Compute. +Under the hood, the Globus Compute SDK uses the following clients to interact with +the Globus Compute API. Advanced users may choose to work directly with these clients +for custom implementations. + +The canonical :class:`ComputeClient` is a subclass of :class:`ComputeClientV2`, +which supports version 2 of the Globus Compute API. When feasible, new projects +should use :class:`ComputeClientV3`, which supports version 3 and includes the +latest API features and improvements. + .. autoclass:: ComputeClient :members: :member-order: bysource :show-inheritance: :exclude-members: error_class, scopes +.. autoclass:: ComputeClientV2 + :members: + :member-order: bysource + :show-inheritance: + :exclude-members: error_class, scopes + + .. attribute:: scopes + + .. listknownscopes:: globus_sdk.scopes.ComputeScopes + :base_name: ComputeClient.scopes + +.. autoclass:: ComputeClientV3 + :members: + :member-order: bysource + :show-inheritance: + :exclude-members: error_class, scopes + .. attribute:: scopes .. listknownscopes:: globus_sdk.scopes.ComputeScopes :base_name: ComputeClient.scopes + Client Errors ------------- diff --git a/src/globus_sdk/__init__.py b/src/globus_sdk/__init__.py index 1b94b1dd1..9bc0a990b 100644 --- a/src/globus_sdk/__init__.py +++ b/src/globus_sdk/__init__.py @@ -70,6 +70,8 @@ def _force_eager_imports() -> None: }, "services.compute": { "ComputeClient", + "ComputeClientV2", + "ComputeClientV3", "ComputeAPIError", "ComputeFunctionDocument", "ComputeFunctionMetadata", @@ -206,6 +208,8 @@ def _force_eager_imports() -> None: from .services.auth import OAuthTokenResponse from .services.auth import DependentScopeSpec from .services.compute import ComputeClient + from .services.compute import ComputeClientV2 + from .services.compute import ComputeClientV3 from .services.compute import ComputeAPIError from .services.compute import ComputeFunctionDocument from .services.compute import ComputeFunctionMetadata @@ -333,6 +337,8 @@ def __getattr__(name: str) -> t.Any: "CollectionPolicies", "ComputeAPIError", "ComputeClient", + "ComputeClientV2", + "ComputeClientV3", "ComputeFunctionDocument", "ComputeFunctionMetadata", "ConfidentialAppAuthClient", diff --git a/src/globus_sdk/_generate_init.py b/src/globus_sdk/_generate_init.py index e39824b61..20f2002cb 100755 --- a/src/globus_sdk/_generate_init.py +++ b/src/globus_sdk/_generate_init.py @@ -135,6 +135,8 @@ def __getattr__(name: str) -> t.Any: "services.compute", ( "ComputeClient", + "ComputeClientV2", + "ComputeClientV3", "ComputeAPIError", "ComputeFunctionDocument", "ComputeFunctionMetadata", diff --git a/src/globus_sdk/_testing/data/compute/v2/__init__.py b/src/globus_sdk/_testing/data/compute/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/globus_sdk/_testing/data/compute/delete_function.py b/src/globus_sdk/_testing/data/compute/v2/delete_function.py similarity index 90% rename from src/globus_sdk/_testing/data/compute/delete_function.py rename to src/globus_sdk/_testing/data/compute/v2/delete_function.py index aca936719..92a65c4b4 100644 --- a/src/globus_sdk/_testing/data/compute/delete_function.py +++ b/src/globus_sdk/_testing/data/compute/v2/delete_function.py @@ -1,6 +1,6 @@ from globus_sdk._testing.models import RegisteredResponse, ResponseSet -from ._common import FUNCTION_ID +from .._common import FUNCTION_ID RESPONSES = ResponseSet( metadata={"function_id": FUNCTION_ID}, diff --git a/src/globus_sdk/_testing/data/compute/get_function.py b/src/globus_sdk/_testing/data/compute/v2/get_function.py similarity index 90% rename from src/globus_sdk/_testing/data/compute/get_function.py rename to src/globus_sdk/_testing/data/compute/v2/get_function.py index 06960b8b4..21339526e 100644 --- a/src/globus_sdk/_testing/data/compute/get_function.py +++ b/src/globus_sdk/_testing/data/compute/v2/get_function.py @@ -1,6 +1,6 @@ from globus_sdk._testing.models import RegisteredResponse, ResponseSet -from ._common import FUNCTION_CODE, FUNCTION_ID, FUNCTION_NAME +from .._common import FUNCTION_CODE, FUNCTION_ID, FUNCTION_NAME FUNCTION_DOC = { "function_uuid": FUNCTION_ID, diff --git a/src/globus_sdk/_testing/data/compute/register_function.py b/src/globus_sdk/_testing/data/compute/v2/register_function.py similarity index 86% rename from src/globus_sdk/_testing/data/compute/register_function.py rename to src/globus_sdk/_testing/data/compute/v2/register_function.py index e0fa0a251..b0c68bd72 100644 --- a/src/globus_sdk/_testing/data/compute/register_function.py +++ b/src/globus_sdk/_testing/data/compute/v2/register_function.py @@ -1,6 +1,6 @@ from globus_sdk._testing.models import RegisteredResponse, ResponseSet -from ._common import FUNCTION_CODE, FUNCTION_ID, FUNCTION_NAME +from .._common import FUNCTION_CODE, FUNCTION_ID, FUNCTION_NAME RESPONSES = ResponseSet( metadata={ diff --git a/src/globus_sdk/services/compute/__init__.py b/src/globus_sdk/services/compute/__init__.py index 6b38facd8..7380734ee 100644 --- a/src/globus_sdk/services/compute/__init__.py +++ b/src/globus_sdk/services/compute/__init__.py @@ -1,10 +1,12 @@ -from .client import ComputeClient +from .client import ComputeClient, ComputeClientV2, ComputeClientV3 from .data import ComputeFunctionDocument, ComputeFunctionMetadata from .errors import ComputeAPIError __all__ = ( "ComputeAPIError", "ComputeClient", + "ComputeClientV2", + "ComputeClientV3", "ComputeFunctionDocument", "ComputeFunctionMetadata", ) diff --git a/src/globus_sdk/services/compute/client.py b/src/globus_sdk/services/compute/client.py index a9fc6a4b0..2351f7d39 100644 --- a/src/globus_sdk/services/compute/client.py +++ b/src/globus_sdk/services/compute/client.py @@ -12,11 +12,11 @@ log = logging.getLogger(__name__) -class ComputeClient(client.BaseClient): +class ComputeClientV2(client.BaseClient): r""" - Client for the Globus Compute API. + Client for the Globus Compute API, version 2. - .. automethodlist:: globus_sdk.ComputeClient + .. automethodlist:: globus_sdk.ComputeClientV2 """ error_class = ComputeAPIError @@ -71,3 +71,25 @@ def delete_function(self, function_id: UUIDLike) -> GlobusHTTPResponse: :ref: Functions/operation/delete_function_v2_functions__function_uuid__delete """ # noqa: E501 return self.delete(f"/v2/functions/{function_id}") + + +class ComputeClientV3(client.BaseClient): + r""" + Client for the Globus Compute API, version 3. + + .. automethodlist:: globus_sdk.ComputeClientV3 + """ + + error_class = ComputeAPIError + service_name = "compute" + scopes = ComputeScopes + default_scope_requirements = [Scope(ComputeScopes.all)] + + +class ComputeClient(ComputeClientV2): + r""" + Canonical client for the Globus Compute API, with support exclusively for + API version 2. + + .. automethodlist:: globus_sdk.ComputeClient + """ diff --git a/tests/functional/services/compute/conftest.py b/tests/functional/services/compute/conftest.py index 820157ae3..2bc2b2ab5 100644 --- a/tests/functional/services/compute/conftest.py +++ b/tests/functional/services/compute/conftest.py @@ -4,8 +4,8 @@ @pytest.fixture -def compute_client(no_retry_transport): - class CustomComputeClient(globus_sdk.ComputeClient): +def compute_client_v2(no_retry_transport): + class CustomComputeClientV2(globus_sdk.ComputeClientV2): transport_class = no_retry_transport - return CustomComputeClient() + return CustomComputeClientV2() diff --git a/tests/functional/services/compute/test_delete_function.py b/tests/functional/services/compute/test_delete_function.py deleted file mode 100644 index 7edac5a76..000000000 --- a/tests/functional/services/compute/test_delete_function.py +++ /dev/null @@ -1,9 +0,0 @@ -import globus_sdk -from globus_sdk._testing import load_response - - -def test_delete_function(compute_client: globus_sdk.ComputeClient): - meta = load_response(compute_client.delete_function).metadata - res = compute_client.delete_function(function_id=meta["function_id"]) - assert res.http_status == 200 - assert res.data == {"result": 302} diff --git a/tests/functional/services/compute/v2/test_delete_function.py b/tests/functional/services/compute/v2/test_delete_function.py new file mode 100644 index 000000000..eab3f08b4 --- /dev/null +++ b/tests/functional/services/compute/v2/test_delete_function.py @@ -0,0 +1,9 @@ +import globus_sdk +from globus_sdk._testing import load_response + + +def test_delete_function(compute_client_v2: globus_sdk.ComputeClientV2): + meta = load_response(compute_client_v2.delete_function).metadata + res = compute_client_v2.delete_function(function_id=meta["function_id"]) + assert res.http_status == 200 + assert res.data == {"result": 302} diff --git a/tests/functional/services/compute/test_get_function.py b/tests/functional/services/compute/v2/test_get_function.py similarity index 57% rename from tests/functional/services/compute/test_get_function.py rename to tests/functional/services/compute/v2/test_get_function.py index f6c03e292..1da30dbea 100644 --- a/tests/functional/services/compute/test_get_function.py +++ b/tests/functional/services/compute/v2/test_get_function.py @@ -2,9 +2,9 @@ from globus_sdk._testing import load_response -def test_get_function(compute_client: globus_sdk.ComputeClient): - meta = load_response(compute_client.get_function).metadata - res = compute_client.get_function(function_id=meta["function_id"]) +def test_get_function(compute_client_v2: globus_sdk.ComputeClientV2): + meta = load_response(compute_client_v2.get_function).metadata + res = compute_client_v2.get_function(function_id=meta["function_id"]) assert res.http_status == 200 assert res.data["function_uuid"] == meta["function_id"] assert res.data["function_name"] == meta["function_name"] diff --git a/tests/functional/services/compute/test_register_function.py b/tests/functional/services/compute/v2/test_register_function.py similarity index 56% rename from tests/functional/services/compute/test_register_function.py rename to tests/functional/services/compute/v2/test_register_function.py index 12fa17125..20b0e966d 100644 --- a/tests/functional/services/compute/test_register_function.py +++ b/tests/functional/services/compute/v2/test_register_function.py @@ -2,12 +2,12 @@ from globus_sdk._testing import load_response -def test_register_function(compute_client: globus_sdk.ComputeClient): - meta = load_response(compute_client.register_function).metadata +def test_register_function(compute_client_v2: globus_sdk.ComputeClientV2): + meta = load_response(compute_client_v2.register_function).metadata registration_doc = { "function_name": meta["function_name"], "function_code": meta["function_code"], } - res = compute_client.register_function(function_data=registration_doc) + res = compute_client_v2.register_function(function_data=registration_doc) assert res.http_status == 200 assert res.data["function_uuid"] == meta["function_id"] diff --git a/tests/unit/services/compute/test_canononical_client.py b/tests/unit/services/compute/test_canononical_client.py new file mode 100644 index 000000000..73ae167e6 --- /dev/null +++ b/tests/unit/services/compute/test_canononical_client.py @@ -0,0 +1,6 @@ +from globus_sdk.services.compute.client import ComputeClient, ComputeClientV2 + + +def test_canonical_client_is_v2(): + client = ComputeClient() + assert isinstance(client, ComputeClientV2) From 4fb701226ae616676531061c4f4f475fbe47fc35 Mon Sep 17 00:00:00 2001 From: Reid Mello <30907815+rjmello@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:40:37 -0400 Subject: [PATCH 2/3] Add changelog for versioned Compute clients --- ...31_151930_30907815+rjmello_versioned_compute_clients.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog.d/20241031_151930_30907815+rjmello_versioned_compute_clients.rst diff --git a/changelog.d/20241031_151930_30907815+rjmello_versioned_compute_clients.rst b/changelog.d/20241031_151930_30907815+rjmello_versioned_compute_clients.rst new file mode 100644 index 000000000..af3c411fb --- /dev/null +++ b/changelog.d/20241031_151930_30907815+rjmello_versioned_compute_clients.rst @@ -0,0 +1,6 @@ +Added +~~~~~ + +- Created ``ComputeClientV2`` and ``ComputeClientV3`` classes to support Globus Compute + API versions 2 and 3, respectively. The canonical ``ComputeClient`` is now a subclass + of ``ComputeClientV2``, preserving backward compatibility. (:pr:`NUMBER`) From 7d63262276dc926139971b8a641010767aed5bdf Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 31 Oct 2024 17:10:31 -0500 Subject: [PATCH 3/3] Map versioned client names to versioned fixtures When mapping client objects (by name) to directories in the _testing data modules, the current mapping inspects `service_name` and the method name. As a result, it's not possible to have multiple versioned clients with the same service name mapped to different fixture directories. To resolve, we need *some* solution which maps versioned clients to versioned fixtures. This change applies one particular solution, which will work in the near-term for ComputeClientV2 and ComputeClientV3, but may be refined further in the future (so long as the mapping of the V2 client to known fixture data is preserved). Rather than ignoring the possibility of structured information in the name of a client, if it ends with `V` for some integer `N`, we can map this to a fixture dir named, similarly, `v` (lowercased). This allows the V2 and V3 clients to get their own fixture directories. One downside of this approach is that it maps `ComputeClient` (which inherits from the V2 client) differently from `ComputeClientV2`, and there is no step here taken to rectify this. Effectively, this means that `ComputeClient` will get no fixture data, but `ComputeClientV2` will, even for identical usages. Alternative approaches abound -- e.g., we could attach a class-level attribute which declares a "test_fixture_subdir" or "api_version", and use that attribute when present. The current solution was chosen as - generic, in that it does not encode Compute-specific details into a helper buried in `_testing` - easy to implement and reason about (it's just a quick suffix match and string conversion) - easy to change or replace in the future, because it makes no changes to the classes which are fed into this mapping --- src/globus_sdk/_testing/registry.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/globus_sdk/_testing/registry.py b/src/globus_sdk/_testing/registry.py index 0577392d5..0f9c3cee9 100644 --- a/src/globus_sdk/_testing/registry.py +++ b/src/globus_sdk/_testing/registry.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib +import re import typing as t import responses @@ -9,6 +10,10 @@ from .models import RegisteredResponse, ResponseList, ResponseSet +# matches "V2", "V11", etc as a string suffix +# see usage in _resolve_qualname for details +_SUFFIX_VERSION_MATCH_PATTERN = re.compile(r"V\d+$") + _RESPONSE_SET_REGISTRY: dict[t.Any, ResponseSet] = {} @@ -53,6 +58,16 @@ def _resolve_qualname(name: str) -> str: assert issubclass(maybe_client, globus_sdk.BaseClient) service_name = maybe_client.service_name + + # TODO: Consider alternative strategies for mapping versioned clients + # to subdirs. For now, we do it by name matching. + # + # 'prefix' is the client name, and it may end in `V2`, `V3`, etc. + # in which case we want to map it to a subdir + suffix_version_match = _SUFFIX_VERSION_MATCH_PATTERN.search(prefix) + if suffix_version_match: + suffix = f"{suffix_version_match.group(0).lower()}.{suffix}" + return f"{service_name}.{suffix}"