Skip to content

Commit b0ee659

Browse files
authored
Add experimental GCSCollectionClient (#1375)
2 parents 73e9acd + 4cceedc commit b0ee659

5 files changed

Lines changed: 167 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Added
2+
-----
3+
4+
- Added a new ``GCSCollectionClient`` class in
5+
``globus_sdk.experimental.gcs_collection_client``.
6+
The new client has no methods other than the base HTTP ones, but contains the
7+
collection ID and scopes in the correct locations for the SDK token management
8+
mechanisms to use. (:pr:`NUMBER`)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.. _gcs_collection_client:
2+
3+
.. currentmodule:: globus_sdk.experimental.gcs_collection_client
4+
5+
GCS Collection Client
6+
=====================
7+
8+
The ``GCSCollectionClient`` class provides an interface for collections, as
9+
resource servers.
10+
It should not be confused with ``globus_sdk.GCSClient``, which provides an
11+
interface to GCS Endpoints.
12+
13+
.. autoclass:: GCSCollectionClient
14+
:members:
15+
:member-order: bysource

docs/experimental/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ Globus SDK Experimental Components
1010

1111
**Use at your own risk.**
1212

13+
.. toctree::
14+
:caption: Experimental Constructs
15+
:maxdepth: 1
16+
17+
gcs_collection_client
1318

1419
Experimental Construct Lifecycle
1520
--------------------------------
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
import uuid
5+
6+
import globus_sdk
7+
from globus_sdk.authorizers import GlobusAuthorizer
8+
from globus_sdk.scopes import GCSCollectionScopes
9+
10+
11+
# NOTE: this stub class idea is inspired by the SpecificFlowScopes class stub
12+
# it implements the same interface as the base class, so it's type-compatible
13+
# but it raises errors at runtime -- because it can't *actually* be populated with data
14+
class _GCSCollectionScopesClassStub(GCSCollectionScopes):
15+
"""
16+
This internal stub object ensures that the type deductions for type checkers (e.g.
17+
mypy) on GCSCollectionClient.scopes are correct.
18+
19+
Primarily, it should be possible to access the `scopes` attribute, the `user`
20+
scope, and the `resource_server`, but these usages should raise specific and
21+
informative runtime errors.
22+
23+
Our types are therefore less accurate for class-var access, but more accurate for
24+
instance-var access.
25+
"""
26+
27+
def __init__(self) -> None:
28+
super().__init__("<stub>")
29+
30+
def __getattribute__(self, name: str) -> t.Any:
31+
if name == "https":
32+
_raise_attr_error("https")
33+
if name == "data_access":
34+
_raise_attr_error("data_access")
35+
if name == "resource_server":
36+
_raise_attr_error("resource_server")
37+
return object.__getattribute__(self, name)
38+
39+
40+
class GCSCollectionClient(globus_sdk.BaseClient):
41+
"""
42+
A client for interacting directly with a GCS Collection.
43+
Typically for HTTPS upload/download via HTTPS-enabled collections.
44+
45+
.. sdk-sphinx-copy-params:: BaseClient
46+
47+
:param collection_id: The ID of the collection.
48+
:param collection_address: The URL of the collection, as might be retrieved from
49+
the ``https_server`` field in Globus Transfer.
50+
"""
51+
52+
scopes: GCSCollectionScopes = _GCSCollectionScopesClassStub()
53+
54+
def __init__(
55+
self,
56+
collection_id: str | uuid.UUID,
57+
collection_address: str,
58+
*,
59+
environment: str | None = None,
60+
app: globus_sdk.GlobusApp | None = None,
61+
app_scopes: list[globus_sdk.scopes.Scope] | None = None,
62+
authorizer: GlobusAuthorizer | None = None,
63+
app_name: str | None = None,
64+
transport: globus_sdk.transport.RequestsTransport | None = None,
65+
retry_config: globus_sdk.transport.RetryConfig | None = None,
66+
) -> None:
67+
self.collection_id = str(collection_id)
68+
self.scopes = GCSCollectionScopes(self.collection_id)
69+
70+
if not collection_address.startswith("https://"):
71+
collection_address = f"https://{collection_address}"
72+
73+
super().__init__(
74+
environment=environment,
75+
base_url=collection_address,
76+
app=app,
77+
app_scopes=app_scopes,
78+
authorizer=authorizer,
79+
app_name=app_name,
80+
transport=transport,
81+
retry_config=retry_config,
82+
)
83+
84+
@property
85+
def default_scope_requirements(self) -> list[globus_sdk.Scope]:
86+
return [self.scopes.https]
87+
88+
89+
def _raise_attr_error(name: str) -> t.NoReturn:
90+
raise AttributeError(
91+
f"It is not valid to attempt to access the '{name}' attribute of the "
92+
"GCSCollectionClient class. "
93+
f"Instead, instantiate a GCSCollectionClient and access the '{name}' attribute "
94+
"from that instance."
95+
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest
2+
3+
import globus_sdk
4+
from globus_sdk.experimental.gcs_collection_client import GCSCollectionClient
5+
6+
7+
@pytest.mark.parametrize("attrname", ["https", "data_access", "resource_server"])
8+
def test_class_level_scopes_access_raises_useful_attribute_error(attrname):
9+
with pytest.raises(
10+
AttributeError,
11+
match=(
12+
f"It is not valid to attempt to access the '{attrname}' attribute of "
13+
"the GCSCollectionClient class"
14+
),
15+
):
16+
getattr(GCSCollectionClient.scopes, attrname)
17+
18+
19+
def test_instance_level_scopes_access_ok():
20+
client = GCSCollectionClient("foo_id", "https://example.com/foo")
21+
22+
assert client.resource_server == "foo_id"
23+
assert client.scopes.https == globus_sdk.Scope(
24+
"https://auth.globus.org/scopes/foo_id/https"
25+
)
26+
assert client.scopes.data_access == globus_sdk.Scope(
27+
"https://auth.globus.org/scopes/foo_id/data_access"
28+
)
29+
30+
31+
def test_default_scope_is_https():
32+
client = GCSCollectionClient("foo_id", "https://example.com/foo")
33+
34+
assert client.default_scope_requirements == [
35+
globus_sdk.Scope("https://auth.globus.org/scopes/foo_id/https")
36+
]
37+
38+
39+
# this behavior imitates the `GCSClient`, which accepts a bare hostname and prepends the
40+
# scheme if it is missing
41+
def test_https_scheme_is_added_to_bare_address():
42+
client = GCSCollectionClient("foo_id", "example.com")
43+
44+
assert client.base_url == "https://example.com"

0 commit comments

Comments
 (0)