dspace-api-toolkit 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dspace_api/__init__.py +75 -0
- dspace_api/_base.py +407 -0
- dspace_api/_containers.py +217 -0
- dspace_api/_version.py +4 -0
- dspace_api/auth.py +467 -0
- dspace_api/authorizations.py +486 -0
- dspace_api/bundles.py +1193 -0
- dspace_api/collections.py +1069 -0
- dspace_api/communities.py +363 -0
- dspace_api/config.py +254 -0
- dspace_api/discovery.py +920 -0
- dspace_api/entity_types.py +127 -0
- dspace_api/epersons.py +647 -0
- dspace_api/exceptions.py +110 -0
- dspace_api/groups.py +599 -0
- dspace_api/items.py +549 -0
- dspace_api/metadata.py +742 -0
- dspace_api/permissions.py +1052 -0
- dspace_api/projection.py +292 -0
- dspace_api/relationships.py +694 -0
- dspace_api/session.py +538 -0
- dspace_api/site.py +98 -0
- dspace_api/utils.py +519 -0
- dspace_api/versions.py +292 -0
- dspace_api/vocabulary.py +365 -0
- dspace_api/workflow.py +893 -0
- dspace_api/workspaces.py +1404 -0
- dspace_api_toolkit-0.1.1.dist-info/METADATA +211 -0
- dspace_api_toolkit-0.1.1.dist-info/RECORD +32 -0
- dspace_api_toolkit-0.1.1.dist-info/WHEEL +5 -0
- dspace_api_toolkit-0.1.1.dist-info/licenses/LICENSE +28 -0
- dspace_api_toolkit-0.1.1.dist-info/top_level.txt +1 -0
dspace_api/groups.py
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
EPerson group management for DSpace API (DSpace 7+ / DSpace-CRIS 7+)
|
|
4
|
+
|
|
5
|
+
Groups
|
|
6
|
+
GET /eperson/groups list
|
|
7
|
+
POST /eperson/groups create
|
|
8
|
+
GET /eperson/groups/{uuid}
|
|
9
|
+
PATCH /eperson/groups/{uuid} rename (/name), metadata
|
|
10
|
+
DELETE /eperson/groups/{uuid}
|
|
11
|
+
GET /eperson/groups/{uuid}/object linked community/collection (role groups)
|
|
12
|
+
GET /eperson/groups/search/byMetadata?query=
|
|
13
|
+
GET /eperson/groups/search/isNotMemberOf?group=&query=
|
|
14
|
+
|
|
15
|
+
Members
|
|
16
|
+
GET|POST /eperson/groups/{uuid}/subgroups (POST body: text/uri-list)
|
|
17
|
+
DELETE /eperson/groups/{uuid}/subgroups/{subgroupUuid}
|
|
18
|
+
GET|POST /eperson/groups/{uuid}/epersons (POST body: text/uri-list)
|
|
19
|
+
DELETE /eperson/groups/{uuid}/epersons/{epersonUuid}
|
|
20
|
+
|
|
21
|
+
Role groups of communities and collections
|
|
22
|
+
GET|POST|DELETE /core/communities/{uuid}/adminGroup
|
|
23
|
+
GET|POST|DELETE /core/collections/{uuid}/adminGroup
|
|
24
|
+
/core/collections/{uuid}/submittersGroup
|
|
25
|
+
/core/collections/{uuid}/itemReadGroup default READ for new items
|
|
26
|
+
/core/collections/{uuid}/bitstreamReadGroup default READ for new files
|
|
27
|
+
/core/collections/{uuid}/workflowGroups/{role} e.g. reviewer, editor, finaleditor
|
|
28
|
+
|
|
29
|
+
Groups and epersons can be referenced by UUID, by HAL href, by the object
|
|
30
|
+
dict the API returned, or by name (groups) / email (epersons).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
35
|
+
|
|
36
|
+
from ._base import BaseManager
|
|
37
|
+
from .projection import EmbedArg, ProjectionArg
|
|
38
|
+
from .config import Endpoints
|
|
39
|
+
from .exceptions import NotFoundError
|
|
40
|
+
|
|
41
|
+
# Logger for the module
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
# API endpoints for DSpace groups and role groups
|
|
45
|
+
GROUPS = getattr(Endpoints, "GROUPS", "/eperson/groups")
|
|
46
|
+
EPERSONS = getattr(Endpoints, "EPERSONS", "/eperson/epersons")
|
|
47
|
+
COMMUNITIES = getattr(Endpoints, "COMMUNITIES", "/core/communities")
|
|
48
|
+
COLLECTIONS = getattr(Endpoints, "COLLECTIONS", "/core/collections")
|
|
49
|
+
|
|
50
|
+
# Role group endpoints per DSpace object type
|
|
51
|
+
ADMIN_GROUP = "adminGroup"
|
|
52
|
+
SUBMITTERS_GROUP = "submittersGroup"
|
|
53
|
+
ITEM_READ_GROUP = "itemReadGroup"
|
|
54
|
+
BITSTREAM_READ_GROUP = "bitstreamReadGroup"
|
|
55
|
+
WORKFLOW_GROUP_PREFIX = "workflowGroups/"
|
|
56
|
+
|
|
57
|
+
# Role groups for communities and collections
|
|
58
|
+
COMMUNITY_ROLES = (ADMIN_GROUP,)
|
|
59
|
+
COLLECTION_ROLES = (ADMIN_GROUP, SUBMITTERS_GROUP, ITEM_READ_GROUP, BITSTREAM_READ_GROUP)
|
|
60
|
+
_DSO_PATHS = {"community": COMMUNITIES, "collection": COLLECTIONS}
|
|
61
|
+
|
|
62
|
+
Ref = Union[str, Dict[str, Any]]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GroupManager(BaseManager):
|
|
66
|
+
"""Manages DSpace groups, their members, and community/collection role groups"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, auth):
|
|
69
|
+
"""Initialize the GroupManager with authentication and configuration.
|
|
70
|
+
|
|
71
|
+
Parameters:
|
|
72
|
+
auth: Authentication object containing session and configuration.
|
|
73
|
+
"""
|
|
74
|
+
super().__init__(auth)
|
|
75
|
+
self._name_cache: Dict[str, str] = {}
|
|
76
|
+
self._eperson_manager = None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def epersons(self):
|
|
80
|
+
"""EPersonManager used to resolve eperson references given by email."""
|
|
81
|
+
if self._eperson_manager is None:
|
|
82
|
+
from .epersons import EPersonManager
|
|
83
|
+
self._eperson_manager = EPersonManager(self.auth)
|
|
84
|
+
return self._eperson_manager
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# Groups
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def get_group(self, group: Ref, embed_object: bool = False, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
91
|
+
"""Retrieve a group by its UUID or reference.
|
|
92
|
+
|
|
93
|
+
Parameters:
|
|
94
|
+
group (Ref): UUID or reference of the group.
|
|
95
|
+
embed_object (bool): Whether to embed the full object.
|
|
96
|
+
all_languages (Optional[bool]): Whether to include all languages.
|
|
97
|
+
projection (ProjectionArg): Projection argument for the request.
|
|
98
|
+
embed (EmbedArg): Embed argument for the request.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dict[str, Any]: The group details.
|
|
102
|
+
"""
|
|
103
|
+
uuid = self.resolve_group(group)
|
|
104
|
+
params = [("embed", "object")] if embed_object else None
|
|
105
|
+
return self._get_json(self._url(GROUPS, f"/{uuid}"), f"Get group {uuid}", params, all_languages=all_languages, projection=projection, embed=embed)
|
|
106
|
+
|
|
107
|
+
def iter_groups(self, page_size: Optional[int] = None, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None):
|
|
108
|
+
"""Every group in the repository (admin only). Can be a lot.
|
|
109
|
+
|
|
110
|
+
Parameters:
|
|
111
|
+
page_size (Optional[int]): Number of groups per page.
|
|
112
|
+
all_languages (Optional[bool]): Whether to include all languages.
|
|
113
|
+
projection (ProjectionArg): Projection argument for the request.
|
|
114
|
+
embed (EmbedArg): Embed argument for the request.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Iterator[Dict[str, Any]]: An iterator over the groups.
|
|
118
|
+
"""
|
|
119
|
+
return self._iter_embedded(self._url(GROUPS), "groups", "List groups", page_size=page_size,
|
|
120
|
+
all_languages=all_languages, projection=projection, embed=embed)
|
|
121
|
+
|
|
122
|
+
def search_groups(self, query: str, max_results: Optional[int] = None,
|
|
123
|
+
embed_object: bool = False, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
124
|
+
"""Search by name or exact UUID (byMetadata).
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
query (str): Search query for group names or exact UUID.
|
|
128
|
+
max_results (Optional[int]): Maximum number of results to return.
|
|
129
|
+
embed_object (bool): Whether to embed the full object.
|
|
130
|
+
all_languages (Optional[bool]): Whether to include all languages.
|
|
131
|
+
projection (ProjectionArg): Projection argument for the request.
|
|
132
|
+
embed (EmbedArg): Embed argument for the request.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List[Dict[str, Any]]: A list of groups matching the search query.
|
|
136
|
+
"""
|
|
137
|
+
params = [("query", query)] + ([("embed", "object")] if embed_object else [])
|
|
138
|
+
url = self._url(GROUPS, "/search/byMetadata")
|
|
139
|
+
return list(self._iter_embedded(url, "groups", f"Search groups '{query}'", params, max_results=max_results,
|
|
140
|
+
all_languages=all_languages, projection=projection, embed=embed))
|
|
141
|
+
|
|
142
|
+
def search_groups_not_member_of(self, group: Ref, query: str = "", max_results: Optional[int] = None,
|
|
143
|
+
embed_object: bool = True) -> List[Dict[str, Any]]:
|
|
144
|
+
"""Groups that could be added as subgroups of `group` (isNotMemberOf).
|
|
145
|
+
|
|
146
|
+
Parameters:
|
|
147
|
+
group (Ref): UUID or reference of the parent group.
|
|
148
|
+
query (str): Search query for group names.
|
|
149
|
+
max_results (Optional[int]): Maximum number of results to return.
|
|
150
|
+
embed_object (bool): Whether to embed the full object.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List[Dict[str, Any]]: A list of groups that can be added as subgroups.
|
|
154
|
+
"""
|
|
155
|
+
params = [("group", self.resolve_group(group)), ("query", query)]
|
|
156
|
+
if embed_object:
|
|
157
|
+
params.append(("embed", "object"))
|
|
158
|
+
url = self._url(GROUPS, "/search/isNotMemberOf")
|
|
159
|
+
return list(self._iter_embedded(url, "groups", "Search addable groups", params, max_results=max_results))
|
|
160
|
+
|
|
161
|
+
def find_group_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
|
162
|
+
"""Exact (case-insensitive) name match, or None."""
|
|
163
|
+
for group in self.search_groups(name):
|
|
164
|
+
if (group.get("name") or "").lower() == name.lower():
|
|
165
|
+
return group
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def resolve_group(self, group: Ref) -> str:
|
|
169
|
+
"""UUID for a group given as UUID, href, dict or name (names are cached).
|
|
170
|
+
|
|
171
|
+
Parameters:
|
|
172
|
+
group (Ref): UUID, href, dict, or name of the group.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
str: UUID of the group.
|
|
176
|
+
"""
|
|
177
|
+
uuid = self._uuid_of(group)
|
|
178
|
+
if uuid:
|
|
179
|
+
return uuid
|
|
180
|
+
if not isinstance(group, str) or not group:
|
|
181
|
+
raise ValueError(f"Cannot resolve group reference {group!r}")
|
|
182
|
+
key = group.lower()
|
|
183
|
+
if key not in self._name_cache:
|
|
184
|
+
found = self.find_group_by_name(group)
|
|
185
|
+
if not found:
|
|
186
|
+
raise NotFoundError(f"Group {group!r} not found")
|
|
187
|
+
self._name_cache[key] = found["uuid"]
|
|
188
|
+
return self._name_cache[key]
|
|
189
|
+
|
|
190
|
+
def create_group(self, name: str, description: Optional[str] = None,
|
|
191
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
192
|
+
"""Create a standalone group (admin only). 422 if the name already exists.
|
|
193
|
+
|
|
194
|
+
Parameters:
|
|
195
|
+
name (str): Name of the group to create.
|
|
196
|
+
description (Optional[str]): Description of the group.
|
|
197
|
+
metadata (Optional[Dict[str, Any]]): Metadata for the group.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dict[str, Any]: The created group object.
|
|
201
|
+
"""
|
|
202
|
+
md = dict(metadata or {})
|
|
203
|
+
if description and "dc.description" not in md:
|
|
204
|
+
md["dc.description"] = description
|
|
205
|
+
body = {"name": name, "metadata": self._format_metadata(md)}
|
|
206
|
+
group = self._request("POST", self._url(GROUPS), f"Create group '{name}'", expected=(200, 201),
|
|
207
|
+
headers=self._headers(), json=body).json()
|
|
208
|
+
logger.info("Created group %s (%s)", group.get("uuid"), name)
|
|
209
|
+
return group
|
|
210
|
+
|
|
211
|
+
def get_or_create_group(self, name: str, description: Optional[str] = None) -> Dict[str, Any]:
|
|
212
|
+
"""Get a group by name, or create it if it doesn't exist.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
name (str): Name of the group to get or create.
|
|
216
|
+
description (Optional[str]): Description of the group if it needs to be created.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Dict[str, Any]: The found or created group object.
|
|
220
|
+
"""
|
|
221
|
+
return self.find_group_by_name(name) or self.create_group(name, description)
|
|
222
|
+
|
|
223
|
+
def patch_group(self, group: Ref, operations: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
224
|
+
"""Apply a series of JSON Patch operations to a group.
|
|
225
|
+
|
|
226
|
+
Parameters:
|
|
227
|
+
group (Ref): UUID or reference of the group to patch.
|
|
228
|
+
operations (List[Dict[str, Any]]): List of JSON Patch operations.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dict[str, Any]: The updated group object.
|
|
232
|
+
"""
|
|
233
|
+
uuid = self.resolve_group(group)
|
|
234
|
+
response = self._request("PATCH", self._url(GROUPS, f"/{uuid}"), f"Patch group {uuid}",
|
|
235
|
+
headers=self._headers(), json=operations)
|
|
236
|
+
self._name_cache.clear()
|
|
237
|
+
return response.json()
|
|
238
|
+
|
|
239
|
+
def rename_group(self, group: Ref, new_name: str) -> Dict[str, Any]:
|
|
240
|
+
"""Permanent groups and role groups (part of a DSO) cannot be renamed (422).
|
|
241
|
+
|
|
242
|
+
Parameters:
|
|
243
|
+
group (Ref): UUID or reference of the group to rename.
|
|
244
|
+
new_name (str): New name for the group.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dict[str, Any]: The updated group object.
|
|
248
|
+
"""
|
|
249
|
+
return self.patch_group(group, [{"op": "replace", "path": "/name", "value": new_name}])
|
|
250
|
+
|
|
251
|
+
def update_group_metadata(self, group: Ref, operation: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
252
|
+
"""Update the metadata of a group using JSON Patch operations.
|
|
253
|
+
|
|
254
|
+
Parameters:
|
|
255
|
+
group (Ref): UUID or reference of the group to update.
|
|
256
|
+
operation (str): JSON Patch operation (e.g., "add", "replace", "remove").
|
|
257
|
+
metadata (Dict[str, Any]): Metadata to apply with the operation.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Dict[str, Any]: The updated group object.
|
|
261
|
+
"""
|
|
262
|
+
return self.patch_group(group, self._metadata_patch_ops(operation, metadata))
|
|
263
|
+
|
|
264
|
+
def delete_group(self, group: Ref) -> bool:
|
|
265
|
+
"""Delete a standalone group. Role groups must be removed via delete_role_group()."""
|
|
266
|
+
uuid = self.resolve_group(group)
|
|
267
|
+
self._request("DELETE", self._url(GROUPS, f"/{uuid}"), f"Delete group {uuid}", expected=(204,))
|
|
268
|
+
self._name_cache.clear()
|
|
269
|
+
logger.info("Deleted group %s", uuid)
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
def get_group_object(self, group: Ref) -> Optional[Dict[str, Any]]:
|
|
273
|
+
"""The community/collection a role group belongs to, or None for standalone groups.
|
|
274
|
+
|
|
275
|
+
Parameters:
|
|
276
|
+
group (Ref): UUID or reference of the group.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Optional[Dict[str, Any]]: The linked community/collection object, or None.
|
|
280
|
+
"""
|
|
281
|
+
uuid = self.resolve_group(group)
|
|
282
|
+
return self._get_optional(self._url(GROUPS, f"/{uuid}/object"), f"Get object of group {uuid}")
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# Members: subgroups
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def get_subgroups(self, group: Ref, embed_object: bool = True, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
289
|
+
"""Direct subgroups (with their linked community/collection embedded by default).
|
|
290
|
+
|
|
291
|
+
Parameters:
|
|
292
|
+
group (Ref): UUID or reference of the parent group.
|
|
293
|
+
embed_object (bool): Whether to embed the full object of the subgroups.
|
|
294
|
+
all_languages (Optional[bool]): Whether to include all languages.
|
|
295
|
+
projection (ProjectionArg): Projection argument for the request.
|
|
296
|
+
embed (EmbedArg): Embed argument for the request.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List[Dict[str, Any]]: A list of direct subgroups.
|
|
300
|
+
"""
|
|
301
|
+
uuid = self.resolve_group(group)
|
|
302
|
+
params = [("embed", "object")] if embed_object else None
|
|
303
|
+
# The REST contract documents "_embedded.groups"; some versions use "subgroups"
|
|
304
|
+
url = self._url(GROUPS, f"/{uuid}/subgroups")
|
|
305
|
+
return list(self._iter_embedded(url, ("subgroups", "groups"), f"Get subgroups of {uuid}", params,
|
|
306
|
+
all_languages=all_languages, projection=projection, embed=embed))
|
|
307
|
+
|
|
308
|
+
def add_subgroup(self, group: Ref, subgroup: Ref) -> bool:
|
|
309
|
+
"""
|
|
310
|
+
Add a subgroup to a parent group.
|
|
311
|
+
|
|
312
|
+
Parameters:
|
|
313
|
+
group (Ref): UUID or reference of the parent group.
|
|
314
|
+
subgroup (Ref): UUID or reference of the subgroup to add.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
bool: True if the operation was successful.
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
HTTPError: If the subgroup doesn't exist or the link would create a cycle (422).
|
|
321
|
+
"""
|
|
322
|
+
parent, child = self.resolve_group(group), self.resolve_group(subgroup)
|
|
323
|
+
self._request("POST", self._url(GROUPS, f"/{parent}/subgroups"), f"Add subgroup {child} to {parent}",
|
|
324
|
+
expected=(200, 204), headers=self._headers("text/uri-list"),
|
|
325
|
+
data=self._uri_list(GROUPS, f"/{child}"))
|
|
326
|
+
logger.info("Added subgroup %s to group %s", child, parent)
|
|
327
|
+
return True
|
|
328
|
+
|
|
329
|
+
def remove_subgroup(self, group: Ref, subgroup: Ref) -> bool:
|
|
330
|
+
"""No-op (still 204) if it wasn't a subgroup.
|
|
331
|
+
|
|
332
|
+
Parameters:
|
|
333
|
+
group (Ref): UUID or reference of the parent group.
|
|
334
|
+
subgroup (Ref): UUID or reference of the subgroup to remove.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
bool: True if the operation was successful.
|
|
338
|
+
"""
|
|
339
|
+
parent, child = self.resolve_group(group), self.resolve_group(subgroup)
|
|
340
|
+
self._request("DELETE", self._url(GROUPS, f"/{parent}/subgroups/{child}"),
|
|
341
|
+
f"Remove subgroup {child} from {parent}", expected=(204,))
|
|
342
|
+
logger.info("Removed subgroup %s from group %s", child, parent)
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
# ------------------------------------------------------------------
|
|
346
|
+
# Members: epersons
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
def get_group_epersons(self, group: Ref, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
350
|
+
"""Direct eperson members.
|
|
351
|
+
|
|
352
|
+
Parameters:
|
|
353
|
+
group (Ref): UUID or reference of the group.
|
|
354
|
+
all_languages (Optional[bool]): Whether to include all languages.
|
|
355
|
+
projection (ProjectionArg): Projection argument for the request.
|
|
356
|
+
embed (EmbedArg): Embed argument for the request.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
List[Dict[str, Any]]: A list of direct eperson members.
|
|
360
|
+
"""
|
|
361
|
+
uuid = self.resolve_group(group)
|
|
362
|
+
return list(self._iter_embedded(self._url(GROUPS, f"/{uuid}/epersons"), "epersons",
|
|
363
|
+
f"Get epersons of group {uuid}", all_languages=all_languages, projection=projection, embed=embed))
|
|
364
|
+
|
|
365
|
+
def add_eperson(self, group: Ref, eperson: Ref) -> bool:
|
|
366
|
+
"""Add an eperson to a group.
|
|
367
|
+
|
|
368
|
+
Parameters:
|
|
369
|
+
group (Ref): UUID or reference of the group.
|
|
370
|
+
eperson (Ref): UUID or reference of the eperson to add.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
bool: True if the operation was successful.
|
|
374
|
+
"""
|
|
375
|
+
group_uuid, person_uuid = self.resolve_group(group), self.epersons.resolve_eperson(eperson)
|
|
376
|
+
self._request("POST", self._url(GROUPS, f"/{group_uuid}/epersons"),
|
|
377
|
+
f"Add eperson {person_uuid} to {group_uuid}", expected=(200, 204),
|
|
378
|
+
headers=self._headers("text/uri-list"), data=self._uri_list(EPERSONS, f"/{person_uuid}"))
|
|
379
|
+
logger.info("Added eperson %s to group %s", person_uuid, group_uuid)
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
def remove_eperson(self, group: Ref, eperson: Ref) -> bool:
|
|
383
|
+
"""Remove an eperson from a group.
|
|
384
|
+
|
|
385
|
+
Parameters:
|
|
386
|
+
group (Ref): UUID or reference of the group.
|
|
387
|
+
eperson (Ref): UUID or reference of the eperson to remove.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
bool: True if the operation was successful.
|
|
391
|
+
"""
|
|
392
|
+
group_uuid, person_uuid = self.resolve_group(group), self.epersons.resolve_eperson(eperson)
|
|
393
|
+
self._request("DELETE", self._url(GROUPS, f"/{group_uuid}/epersons/{person_uuid}"),
|
|
394
|
+
f"Remove eperson {person_uuid} from {group_uuid}", expected=(204,))
|
|
395
|
+
logger.info("Removed eperson %s from group %s", person_uuid, group_uuid)
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
# Members: bulk
|
|
400
|
+
# ------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
def add_members(self, group: Ref, epersons: Iterable[Ref] = (), subgroups: Iterable[Ref] = ()) -> Dict[str, List[str]]:
|
|
403
|
+
"""Add epersons and subgroups, skipping those that are already direct members.
|
|
404
|
+
|
|
405
|
+
Parameters:
|
|
406
|
+
group (Ref): UUID or reference of the group.
|
|
407
|
+
epersons (Iterable[Ref], optional): Epersons to add. Defaults to ().
|
|
408
|
+
subgroups (Iterable[Ref], optional): Subgroups to add. Defaults to ().
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Dict[str, List[str]]: A dictionary with keys "added_epersons" and "added_subgroups" listing the added members.
|
|
412
|
+
"""
|
|
413
|
+
uuid = self.resolve_group(group)
|
|
414
|
+
result = {"added_epersons": [], "added_subgroups": []}
|
|
415
|
+
epersons, subgroups = list(epersons), list(subgroups)
|
|
416
|
+
if epersons:
|
|
417
|
+
current = {e["uuid"] for e in self.get_group_epersons(uuid)}
|
|
418
|
+
for ref in epersons:
|
|
419
|
+
person = self.epersons.resolve_eperson(ref)
|
|
420
|
+
if person not in current:
|
|
421
|
+
self.add_eperson(uuid, person)
|
|
422
|
+
current.add(person)
|
|
423
|
+
result["added_epersons"].append(person)
|
|
424
|
+
if subgroups:
|
|
425
|
+
current = {g["uuid"] for g in self.get_subgroups(uuid, embed_object=False)}
|
|
426
|
+
for ref in subgroups:
|
|
427
|
+
child = self.resolve_group(ref)
|
|
428
|
+
if child not in current:
|
|
429
|
+
self.add_subgroup(uuid, child)
|
|
430
|
+
current.add(child)
|
|
431
|
+
result["added_subgroups"].append(child)
|
|
432
|
+
return result
|
|
433
|
+
|
|
434
|
+
def sync_members(self, group: Ref, epersons: Optional[Iterable[Ref]] = None,
|
|
435
|
+
subgroups: Optional[Iterable[Ref]] = None, dry_run: bool = False) -> Dict[str, List[str]]:
|
|
436
|
+
"""
|
|
437
|
+
Make the direct members exactly the given lists. None leaves that kind
|
|
438
|
+
of member untouched; an empty list removes all of them.
|
|
439
|
+
|
|
440
|
+
Parameters:
|
|
441
|
+
group (Ref): UUID or reference of the group.
|
|
442
|
+
epersons (Optional[Iterable[Ref]]): Desired list of eperson members. None leaves unchanged.
|
|
443
|
+
subgroups (Optional[Iterable[Ref]]): Desired list of subgroup members. None leaves unchanged.
|
|
444
|
+
dry_run (bool): If True, only simulate the changes without applying them.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Dict[str, List[str]]: A dictionary with keys "added_epersons", "removed_epersons", "added_subgroups", and "removed_subgroups" listing the changes made.
|
|
448
|
+
"""
|
|
449
|
+
uuid = self.resolve_group(group)
|
|
450
|
+
result = {"added_epersons": [], "removed_epersons": [], "added_subgroups": [], "removed_subgroups": []}
|
|
451
|
+
if epersons is not None:
|
|
452
|
+
wanted = {self.epersons.resolve_eperson(e) for e in epersons}
|
|
453
|
+
current = {e["uuid"] for e in self.get_group_epersons(uuid)}
|
|
454
|
+
result["added_epersons"] = sorted(wanted - current)
|
|
455
|
+
result["removed_epersons"] = sorted(current - wanted)
|
|
456
|
+
if subgroups is not None:
|
|
457
|
+
wanted = {self.resolve_group(g) for g in subgroups}
|
|
458
|
+
current = {g["uuid"] for g in self.get_subgroups(uuid, embed_object=False)}
|
|
459
|
+
result["added_subgroups"] = sorted(wanted - current)
|
|
460
|
+
result["removed_subgroups"] = sorted(current - wanted)
|
|
461
|
+
if dry_run:
|
|
462
|
+
logger.info("[dry-run] sync_members %s: %s", uuid, result)
|
|
463
|
+
return result
|
|
464
|
+
for e in result["added_epersons"]:
|
|
465
|
+
self.add_eperson(uuid, e)
|
|
466
|
+
for e in result["removed_epersons"]:
|
|
467
|
+
self.remove_eperson(uuid, e)
|
|
468
|
+
for g in result["added_subgroups"]:
|
|
469
|
+
self.add_subgroup(uuid, g)
|
|
470
|
+
for g in result["removed_subgroups"]:
|
|
471
|
+
self.remove_subgroup(uuid, g)
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
# ------------------------------------------------------------------
|
|
475
|
+
# Role groups of communities and collections
|
|
476
|
+
# ------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
def _role_url(self, dso_type: str, dso_uuid: str, role: str) -> str:
|
|
479
|
+
"""Construct the URL for a role group of a community or collection.
|
|
480
|
+
|
|
481
|
+
Parameters:
|
|
482
|
+
dso_type (str): Either "community" or "collection".
|
|
483
|
+
dso_uuid (str): UUID of the community or collection.
|
|
484
|
+
role (str): Role name.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
str: The URL for the role group.
|
|
488
|
+
|
|
489
|
+
Raises:
|
|
490
|
+
ValueError: If the dso_type or role is invalid.
|
|
491
|
+
"""
|
|
492
|
+
if dso_type not in _DSO_PATHS:
|
|
493
|
+
raise ValueError(f"dso_type must be 'community' or 'collection', not {dso_type!r}")
|
|
494
|
+
allowed = COMMUNITY_ROLES if dso_type == "community" else COLLECTION_ROLES
|
|
495
|
+
if role not in allowed and not (dso_type == "collection" and role.startswith(WORKFLOW_GROUP_PREFIX)
|
|
496
|
+
and len(role) > len(WORKFLOW_GROUP_PREFIX)):
|
|
497
|
+
raise ValueError(f"Unknown {dso_type} role {role!r}; expected one of {allowed}"
|
|
498
|
+
+ (" or 'workflowGroups/<role>'" if dso_type == "collection" else ""))
|
|
499
|
+
return self._url(_DSO_PATHS[dso_type], f"/{dso_uuid}/{role}")
|
|
500
|
+
|
|
501
|
+
def get_role_group(self, dso_type: str, dso_uuid: str, role: str) -> Optional[Dict[str, Any]]:
|
|
502
|
+
"""
|
|
503
|
+
The role group, or None if it hasn't been created (204).
|
|
504
|
+
|
|
505
|
+
For itemReadGroup / bitstreamReadGroup DSpace returns the group of
|
|
506
|
+
the collection's DEFAULT_*_READ policy - on an unrestricted
|
|
507
|
+
collection that is the Anonymous group itself. That is reported as
|
|
508
|
+
None ("no read group yet"), so nobody adds members to Anonymous by
|
|
509
|
+
accident (which would affect every object readable by Anonymous).
|
|
510
|
+
|
|
511
|
+
Parameters:
|
|
512
|
+
dso_type (str): Either "community" or "collection".
|
|
513
|
+
dso_uuid (str): UUID of the community or collection.
|
|
514
|
+
role (str): Role name.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Optional[Dict[str, Any]]: The role group if it exists, otherwise None.
|
|
518
|
+
"""
|
|
519
|
+
group = self._get_optional(self._role_url(dso_type, dso_uuid, role), f"Get {role} of {dso_type} {dso_uuid}")
|
|
520
|
+
if group and role in (ITEM_READ_GROUP, BITSTREAM_READ_GROUP) and \
|
|
521
|
+
str(group.get("name", "")).lower() == "anonymous":
|
|
522
|
+
return None
|
|
523
|
+
return group
|
|
524
|
+
|
|
525
|
+
def create_role_group(self, dso_type: str, dso_uuid: str, role: str, description: Optional[str] = None,
|
|
526
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
527
|
+
"""
|
|
528
|
+
Create a role group; DSpace names it itself (e.g. COLLECTION_<uuid>_SUBMIT).
|
|
529
|
+
For itemReadGroup / bitstreamReadGroup this replaces the Anonymous
|
|
530
|
+
DEFAULT_*_READ policy of the collection with the new group.
|
|
531
|
+
|
|
532
|
+
Parameters:
|
|
533
|
+
dso_type (str): Either "community" or "collection".
|
|
534
|
+
dso_uuid (str): UUID of the community or collection.
|
|
535
|
+
role (str): Role name.
|
|
536
|
+
description (Optional[str]): Description for the role group.
|
|
537
|
+
metadata (Optional[Dict[str, Any]]): Additional metadata for the role group.
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Dict[str, Any]: The created role group.
|
|
541
|
+
"""
|
|
542
|
+
md = dict(metadata or {})
|
|
543
|
+
if description and "dc.description" not in md:
|
|
544
|
+
md["dc.description"] = description
|
|
545
|
+
url = self._role_url(dso_type, dso_uuid, role)
|
|
546
|
+
group = self._request("POST", url, f"Create {role} of {dso_type} {dso_uuid}", expected=(200, 201),
|
|
547
|
+
headers=self._headers(), json={"metadata": self._format_metadata(md)}).json()
|
|
548
|
+
logger.info("Created %s %s for %s %s", role, group.get("uuid"), dso_type, dso_uuid)
|
|
549
|
+
return group
|
|
550
|
+
|
|
551
|
+
def get_or_create_role_group(self, dso_type: str, dso_uuid: str, role: str,
|
|
552
|
+
description: Optional[str] = None) -> Dict[str, Any]:
|
|
553
|
+
"""Get the role group if it exists, otherwise create it.
|
|
554
|
+
|
|
555
|
+
Parameters:
|
|
556
|
+
dso_type (str): Either "community" or "collection".
|
|
557
|
+
dso_uuid (str): UUID of the community or collection.
|
|
558
|
+
role (str): Role name.
|
|
559
|
+
description (Optional[str], optional): Description for the role group. Defaults to None.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
Dict[str, Any]: The role group.
|
|
563
|
+
"""
|
|
564
|
+
return (self.get_role_group(dso_type, dso_uuid, role)
|
|
565
|
+
or self.create_role_group(dso_type, dso_uuid, role, description))
|
|
566
|
+
|
|
567
|
+
def delete_role_group(self, dso_type: str, dso_uuid: str, role: str) -> bool:
|
|
568
|
+
"""Delete the role group.
|
|
569
|
+
|
|
570
|
+
Parameters:
|
|
571
|
+
dso_type (str): Either "community" or "collection".
|
|
572
|
+
dso_uuid (str): UUID of the community or collection.
|
|
573
|
+
role (str): Role name.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
bool: True if the role group was deleted successfully.
|
|
577
|
+
"""
|
|
578
|
+
self._request("DELETE", self._role_url(dso_type, dso_uuid, role), f"Delete {role} of {dso_type} {dso_uuid}",
|
|
579
|
+
expected=(204,))
|
|
580
|
+
logger.info("Deleted %s of %s %s", role, dso_type, dso_uuid)
|
|
581
|
+
return True
|
|
582
|
+
|
|
583
|
+
def ensure_role_members(self, dso_type: str, dso_uuid: str, role: str, epersons: Iterable[Ref] = (),
|
|
584
|
+
subgroups: Iterable[Ref] = (), description: Optional[str] = None) -> Dict[str, Any]:
|
|
585
|
+
"""Create the role group if needed and add the members. Returns {"group", "added_*"}.
|
|
586
|
+
|
|
587
|
+
Parameters:
|
|
588
|
+
dso_type (str): Either "community" or "collection".
|
|
589
|
+
dso_uuid (str): UUID of the community or collection.
|
|
590
|
+
role (str): Role name.
|
|
591
|
+
epersons (Iterable[Ref], optional): Epersons to add. Defaults to ().
|
|
592
|
+
subgroups (Iterable[Ref], optional): Subgroups to add. Defaults to ().
|
|
593
|
+
description (Optional[str], optional): Description for the role group. Defaults to None.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Dict[str, Any]: A dictionary containing the role group and the added members.
|
|
597
|
+
"""
|
|
598
|
+
group = self.get_or_create_role_group(dso_type, dso_uuid, role, description)
|
|
599
|
+
return {"group": group, **self.add_members(group["uuid"], epersons, subgroups)}
|