Skip to content

Commit 5ef619a

Browse files
committed
Add experimental GCSCollectionClient
The new client definition is minimal, providing no methods. It only maps collection ID + base URL to existing SDK methods, and defines the scopes appropriately for the client. Based on the solution for `scope` as a class-attribute of clients used in the `SpecificFlowClient`, a stub is defined for the scopes of this client class which raises appropriate attribute errors on access. It thereby makes itself type-safe in contexts where it is used correctly. (Adjusting such that scope based access at the class level is a type error for this class and `SpecificFlowClient` is considered out of scope for this change.) Unit tests for the scope behaviors + a new (small) doc page are included.
1 parent 73e9acd commit 5ef619a

5 files changed

Lines changed: 159 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 SpecificFlowClient.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+
.. note::
46+
47+
Because the client communicates directly with paths on the collection, rather
48+
than with the Endpoint hosting it, the ``base_url`` parameter is required.
49+
50+
.. sdk-sphinx-copy-params:: BaseClient
51+
52+
:param collection_id: The ID of the collection.
53+
"""
54+
55+
scopes: GCSCollectionScopes = _GCSCollectionScopesClassStub()
56+
57+
def __init__(
58+
self,
59+
collection_id: str | uuid.UUID,
60+
base_url: str,
61+
*,
62+
environment: str | None = None,
63+
app: globus_sdk.GlobusApp | None = None,
64+
app_scopes: list[globus_sdk.scopes.Scope] | None = None,
65+
authorizer: GlobusAuthorizer | None = None,
66+
app_name: str | None = None,
67+
transport: globus_sdk.transport.RequestsTransport | None = None,
68+
retry_config: globus_sdk.transport.RetryConfig | None = None,
69+
) -> None:
70+
self.collection_id = str(collection_id)
71+
self.scopes = GCSCollectionScopes(self.collection_id)
72+
73+
super().__init__(
74+
environment=environment,
75+
base_url=base_url,
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
]

0 commit comments

Comments
 (0)