Skip to content

Commit 4f42ff3

Browse files
committed
Implement ListStorageSpaces call
1 parent 274fe6e commit 4f42ff3

3 files changed

Lines changed: 176 additions & 0 deletions

File tree

cs3client/cs3client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .app import App
2020
from .checkpoint import Checkpoint
2121
from .config import Config
22+
from .space import Space
2223

2324

2425
class CS3Client:
@@ -54,6 +55,7 @@ def __init__(self, config: ConfigParser, config_category: str, log: logging.Logg
5455
self._config, self._log, self._gateway, self._status_code_handler
5556
)
5657
self.share = Share(self._config, self._log, self._gateway, self._status_code_handler)
58+
self.spaces = Space(self._config, self._log, self._gateway, self._status_code_handler)
5759

5860
def _create_channel(self) -> grpc.Channel:
5961
"""

cs3client/space.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
space.py
3+
4+
Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti.
5+
Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch
6+
Last updated: 23/02/2026
7+
"""
8+
9+
import logging
10+
from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub
11+
12+
from .config import Config
13+
from .statuscodehandler import StatusCodeHandler
14+
import cs3.storage.provider.v1beta1.spaces_api_pb2 as cs3spp
15+
import cs3.storage.provider.v1beta1.resources_pb2 as cs3spr
16+
import cs3.identity.user.v1beta1.resources_pb2 as cs3iur
17+
18+
19+
20+
class Space:
21+
"""
22+
Space class to handle space related API calls with CS3 Gateway API.
23+
"""
24+
25+
def __init__(
26+
self,
27+
config: Config,
28+
log: logging.Logger,
29+
gateway: GatewayAPIStub,
30+
status_code_handler: StatusCodeHandler,
31+
) -> None:
32+
"""
33+
Initializes the Group class with logger, auth, and gateway stub,
34+
35+
:param log: Logger instance for logging.
36+
:param gateway: GatewayAPIStub instance for interacting with CS3 Gateway.
37+
:param auth: An instance of the auth class.
38+
"""
39+
self._log: logging.Logger = log
40+
self._gateway: GatewayAPIStub = gateway
41+
self._config: Config = config
42+
self._status_code_handler: StatusCodeHandler = status_code_handler
43+
44+
def list_storage_spaces(self, auth_token: tuple, filters) -> list[cs3spr.StorageSpace]:
45+
"""
46+
Find a space based on a filter.
47+
48+
:param auth_token: tuple in the form ('x-access-token', <token>) (see auth.get_token/auth.check_token)
49+
:param filters: Filters to search for.
50+
:return: a list of space(s).
51+
:raises: NotFoundException (Space not found)
52+
:raises: AuthenticationException (Operation not permitted)
53+
:raises: UnknownException (Unknown error)
54+
"""
55+
req = cs3spp.ListStorageSpacesRequest(filters=filters)
56+
res = self._gateway.ListStorageSpaces(request=req, metadata=[auth_token])
57+
self._status_code_handler.handle_errors(res.status, "find storage spaces")
58+
self._log.debug(f'msg="Invoked FindStorageSpaces" filter="{filter}" trace="{res.status.trace}"')
59+
return res.storage_spaces
60+
61+
@classmethod
62+
def create_storage_space_filter(cls, filter_type: str, space_type: str = None, path: str = None, opaque_id: str = None, user_idp: str = None, user_type: str = None) -> cs3spp.ListStorageSpacesRequest.Filter:
63+
"""
64+
Create a filter for listing storage spaces.
65+
66+
:param filter_value: Value of the filter.
67+
:param filter_type: Type of the filter. Supported values are "ID", "OWNER", "SPACE_TYPE", "PATH" and "USER".
68+
:param space_type: Space type to filter by (required if filter_type is "SPACE_TYPE").
69+
:param path: Path to filter by (required if filter_type is "PATH").
70+
:param opaque_id: Opaque ID to filter by (required if filter_type is "ID").
71+
:param user_idp: User identity provider to filter by (required if filter_type is "OWNER" or "USER").
72+
:param user_type: User type to filter by (required if filter_type is "OWNER" or "USER").
73+
:param filter_value: Value of the filter.
74+
:return: A cs3spp.ListStorageSpacesRequest.Filter object.
75+
:raises: ValueError (Unsupported filter type)
76+
"""
77+
try:
78+
filter_type_value = cs3spp.ListStorageSpacesRequest.Filter.Type.Value(filter_type)
79+
80+
if filter_type is None:
81+
raise ValueError(f'Unsupported filter type: {filter_type}. Supported values are "Id", "Owner", "SpaceType", "Path" and "User".')
82+
if space_type and filter_type == "TYPE_SPACE_TYPE":
83+
return cs3spp.ListStorageSpacesRequest.Filter(type=filter_type_value, space_type=space_type)
84+
if path and filter_type == "TYPE_PATH":
85+
return cs3spp.ListStorageSpacesRequest.Filter(type=filter_type_value, path=path)
86+
if user_idp and user_type and opaque_id and filter_type == "TYPE_OWNER":
87+
user_type = cs3iur.UserType.Value(user_type.upper())
88+
user_id = cs3iur.UserId(idp=user_idp, type=user_type, opaque_id=opaque_id)
89+
return cs3spp.ListStorageSpacesRequest.Filter(type=filter_type_value, owner=user_id)
90+
if user_idp and user_type and opaque_id and filter_type == "TYPE_USER":
91+
user_type = cs3iur.UserType.Value(user_type.upper())
92+
user_id = cs3iur.UserId(idp=user_idp, type=user_type, opaque_id=opaque_id)
93+
return cs3spp.ListStorageSpacesRequest.Filter(type=filter_type_value, user=user_id)
94+
if opaque_id and filter_type == "TYPE_ID":
95+
id = cs3spr.StorageSpaceId(opaque_id=opaque_id)
96+
return cs3spp.ListStorageSpacesRequest.Filter(type=filter_type_value, id=id)
97+
except ValueError as e:
98+
raise ValueError(f"Failed to create storage space filter: {e}")
99+
return None

tests/test_space.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
test_space.py
3+
4+
Tests that the Space class methods work as expected.
5+
6+
Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti.
7+
Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch
8+
Last updated: 23/02/2026
9+
"""
10+
11+
12+
import pytest
13+
from unittest.mock import Mock, patch
14+
import cs3.rpc.v1beta1.code_pb2 as cs3code
15+
import cs3.storage.provider.v1beta1.spaces_api_pb2 as cs3spp
16+
import cs3.identity.user.v1beta1.resources_pb2 as cs3iur
17+
import cs3.storage.provider.v1beta1.resources_pb2 as cs3spr
18+
19+
from cs3client.exceptions import (
20+
AuthenticationException,
21+
UnknownException,
22+
)
23+
from .fixtures import ( # noqa: F401 (they are used, the framework is not detecting it)
24+
mock_config,
25+
mock_logger,
26+
mock_gateway,
27+
mock_status_code_handler,
28+
)
29+
30+
@pytest.fixture
31+
def space_instance(mock_config, mock_logger, mock_gateway, mock_status_code_handler): # noqa: F811
32+
"""
33+
Fixture for creating a Space instance with mocked dependencies.
34+
"""
35+
from cs3client.space import Space
36+
37+
return Space(mock_config, mock_logger, mock_gateway, mock_status_code_handler)
38+
39+
40+
@pytest.mark.parametrize(
41+
"status_code, status_message, expected_exception",
42+
[
43+
(cs3code.CODE_OK, None, None),
44+
(cs3code.CODE_UNAUTHENTICATED, "error", AuthenticationException),
45+
(cs3code.CODE_INTERNAL, "error", UnknownException),
46+
],
47+
)
48+
def test_list_storage_spaces(space_instance, status_code, status_message, expected_exception): # noqa: F811
49+
mock_response = Mock()
50+
mock_response.status.code = status_code
51+
mock_response.status.message = status_message
52+
mock_response.storage_spaces = ["space1", "space2"]
53+
auth_token = ('x-access-token', "some_token")
54+
55+
with patch.object(space_instance._gateway, "ListStorageSpaces", return_value=mock_response):
56+
if expected_exception:
57+
with pytest.raises(expected_exception):
58+
space_instance.list_storage_spaces(auth_token, filters=[])
59+
else:
60+
result = space_instance.list_storage_spaces(auth_token, filters=[])
61+
assert result == ["space1", "space2"]
62+
63+
@pytest.mark.parametrize(
64+
"filter_type, space_type, path, opaque_id, user_idp, user_type, expected_filter",
65+
[
66+
("TYPE_SPACE_TYPE", "home", None, None, None, None, cs3spp.ListStorageSpacesRequest.Filter(type="TYPE_SPACE_TYPE", space_type="home")),
67+
("TYPE_PATH", None, "/path/to/space", None, None, None, cs3spp.ListStorageSpacesRequest.Filter(type="TYPE_PATH", path="/path/to/space")),
68+
("TYPE_OWNER", None, None, "opaque_id", "user_idp", "USER_TYPE_PRIMARY", cs3spp.ListStorageSpacesRequest.Filter(type="TYPE_OWNER", owner=cs3iur.UserId(idp="user_idp", type=cs3iur.UserType.Value("USER_TYPE_PRIMARY"), opaque_id="opaque_id"))),
69+
("TYPE_USER", None, None, "opaque_id", "user_idp", "USER_TYPE_PRIMARY", cs3spp.ListStorageSpacesRequest.Filter(type="TYPE_USER", user=cs3iur.UserId(idp="user_idp", type=cs3iur.UserType.Value("USER_TYPE_PRIMARY"), opaque_id="opaque_id"))),
70+
("TYPE_ID", None, None, "opaque_id", None, None, cs3spp.ListStorageSpacesRequest.Filter(type="TYPE_ID", id=cs3spr.StorageSpaceId(opaque_id="opaque_id"))),
71+
],
72+
)
73+
def test_create_storage_space_filter(space_instance, filter_type, space_type, path, opaque_id, user_idp, user_type, expected_filter): # noqa: F811
74+
result = space_instance.create_storage_space_filter(filter_type, space_type, path, opaque_id, user_idp, user_type)
75+
assert result == expected_filter

0 commit comments

Comments
 (0)