Skip to content

Commit feb25ff

Browse files
rawe0diocas
authored andcommitted
Implement ListStorageSpaces call
1 parent 274fe6e commit feb25ff

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

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.space = 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 typing import Literal
11+
from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub
12+
13+
from .config import Config
14+
from .statuscodehandler import StatusCodeHandler
15+
import cs3.storage.provider.v1beta1.spaces_api_pb2 as cs3spp
16+
import cs3.storage.provider.v1beta1.resources_pb2 as cs3spr
17+
import cs3.identity.user.v1beta1.resources_pb2 as cs3iur
18+
19+
20+
21+
class Space:
22+
"""
23+
Space class to handle space related API calls with CS3 Gateway API.
24+
"""
25+
26+
def __init__(
27+
self,
28+
config: Config,
29+
log: logging.Logger,
30+
gateway: GatewayAPIStub,
31+
status_code_handler: StatusCodeHandler,
32+
) -> None:
33+
"""
34+
Initializes the Group class with logger, auth, and gateway stub,
35+
36+
:param log: Logger instance for logging.
37+
:param gateway: GatewayAPIStub instance for interacting with CS3 Gateway.
38+
:param auth: An instance of the auth class.
39+
"""
40+
self._log: logging.Logger = log
41+
self._gateway: GatewayAPIStub = gateway
42+
self._config: Config = config
43+
self._status_code_handler: StatusCodeHandler = status_code_handler
44+
45+
def list_storage_spaces(self, auth_token: tuple, filters) -> list[cs3spr.StorageSpace]:
46+
"""
47+
Find a space based on a filter.
48+
49+
:param auth_token: tuple in the form ('x-access-token', <token>) (see auth.get_token/auth.check_token)
50+
:param filters: Filters to search for.
51+
:return: a list of space(s).
52+
:raises: NotFoundException (Space not found)
53+
:raises: AuthenticationException (Operation not permitted)
54+
:raises: UnknownException (Unknown error)
55+
"""
56+
req = cs3spp.ListStorageSpacesRequest(filters=filters)
57+
res = self._gateway.ListStorageSpaces(request=req, metadata=[auth_token])
58+
self._status_code_handler.handle_errors(res.status, "find storage spaces")
59+
self._log.debug(f'msg="Invoked FindStorageSpaces" filter="{filter}" trace="{res.status.trace}"')
60+
return res.storage_spaces
61+
62+
@classmethod
63+
def create_storage_space_filter(cls, filter_type: Literal["TYPE_ID", "TYPE_OWNER", "TYPE_SPACE_TYPE", "TYPE_PATH", "TYPE_USER"], space_type: str = None, path: str = None, opaque_id: str = None, user_idp: str = None, user_type: str = None) -> cs3spp.ListStorageSpacesRequest.Filter:
64+
"""
65+
Create a filter for listing storage spaces.
66+
67+
:param filter_value: Value of the filter.
68+
:param filter_type: Type of the filter. Supported values are "TYPE_ID", "TYPE_OWNER", "TYPE_SPACE_TYPE", "TYPE_PATH" and "TYPE_USER".
69+
:param space_type: Space type to filter by (required if filter_type is "SPACE_TYPE").
70+
:param path: Path to filter by (required if filter_type is "PATH").
71+
:param opaque_id: Opaque ID to filter by (required if filter_type is "ID").
72+
:param user_idp: User identity provider to filter by (required if filter_type is "OWNER" or "USER").
73+
:param user_type: User type to filter by (required if filter_type is "OWNER" or "USER").
74+
:param filter_value: Value of the filter.
75+
:return: A cs3spp.ListStorageSpacesRequest.Filter object.
76+
:raises: ValueError (Unsupported filter type)
77+
"""
78+
try:
79+
if filter_type is None:
80+
raise ValueError(f'Unsupported filter type: {filter_type}. Supported values are "TYPE_ID", "TYPE_OWNER", "TYPE_SPACE_TYPE", "TYPE_PATH" and "TYPE_USER".')
81+
filter_type_value = cs3spp.ListStorageSpacesRequest.Filter.Type.Value(filter_type)
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+
raise ValueError(f'Unsupported filter type: {filter_type}. Supported values are "TYPE_ID", "TYPE_OWNER", "TYPE_SPACE_TYPE", "TYPE_PATH" and "TYPE_USER".')

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)