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
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Collection management for DSpace API
|
|
4
|
+
|
|
5
|
+
GET|POST /core/collections/{uuid}/logo, DELETE via the logo bitstream
|
|
6
|
+
GET /core/collections/{uuid}/parentCommunity
|
|
7
|
+
GET /core/collections/search/findAdminAuthorized|findEditAuthorized
|
|
8
|
+
GET /core/collections/search/findSubmitAuthorized[ByEntityType|ByCommunity|ByCommunityAndEntityType]
|
|
9
|
+
GET /core/collections/{uuid}/license (custom license or the site default)
|
|
10
|
+
GET|POST /core/collections/{uuid}/itemtemplate PATCH|DELETE /core/itemtemplates/{uuid}
|
|
11
|
+
GET /core/collections/{uuid}/defaultAccessConditions
|
|
12
|
+
GET /core/collections/{uuid}/mappedItems POST|DELETE /core/items/{item}/mappedCollections[/{uuid}]
|
|
13
|
+
GET|PUT /core/collections/{uuid}/harvester GET /config/harvestermetadata
|
|
14
|
+
|
|
15
|
+
Logo, parent community, patch_metadata and the *Authorized searches are
|
|
16
|
+
shared with communities (_containers.py).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
21
|
+
from ._base import BaseManager
|
|
22
|
+
from ._containers import ContainerMixin
|
|
23
|
+
from .metadata import MetadataPatch
|
|
24
|
+
from .projection import EmbedArg, ProjectionArg
|
|
25
|
+
from .communities import build_dso_metadata
|
|
26
|
+
from .config import Endpoints
|
|
27
|
+
from .projection import view_params
|
|
28
|
+
from .exceptions import NotFoundError, handle_response_error
|
|
29
|
+
|
|
30
|
+
# logger for this module
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# Collection role groups (see GroupManager)
|
|
34
|
+
ADMIN = "adminGroup"
|
|
35
|
+
SUBMITTERS = "submittersGroup"
|
|
36
|
+
ITEM_READ = "itemReadGroup"
|
|
37
|
+
BITSTREAM_READ = "bitstreamReadGroup"
|
|
38
|
+
|
|
39
|
+
Ref = Union[str, Dict[str, Any]]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_collection_metadata(title: str, description: Optional[str] = None, abstract: Optional[str] = None,
|
|
43
|
+
rights: Optional[str] = None, table_of_contents: Optional[str] = None,
|
|
44
|
+
license: Optional[str] = None, entity_type: Optional[str] = None,
|
|
45
|
+
submission_definition: Optional[str] = None,
|
|
46
|
+
submission_definition_correction: Optional[str] = None,
|
|
47
|
+
workspace_shared: Optional[bool] = None,
|
|
48
|
+
language: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
|
|
49
|
+
"""
|
|
50
|
+
Metadata for a (DSpace-CRIS) collection: the community fields plus
|
|
51
|
+
dc.rights.license, dspace.entity.type, cris.submission.definition,
|
|
52
|
+
cris.submission.definition-correction and cris.workspace.shared.
|
|
53
|
+
|
|
54
|
+
Parameters:
|
|
55
|
+
title (str): The title of the collection.
|
|
56
|
+
description (Optional[str]): The description of the collection.
|
|
57
|
+
abstract (Optional[str]): The abstract of the collection.
|
|
58
|
+
rights (Optional[str]): The rights information of the collection.
|
|
59
|
+
table_of_contents (Optional[str]): The table of contents of the collection.
|
|
60
|
+
license (Optional[str]): The license of the collection.
|
|
61
|
+
entity_type (Optional[str]): The entity type of the collection.
|
|
62
|
+
submission_definition (Optional[str]): The submission definition of the collection.
|
|
63
|
+
submission_definition_correction (Optional[str]): The submission definition correction of the collection.
|
|
64
|
+
workspace_shared (Optional[bool]): Whether the workspace is shared.
|
|
65
|
+
language (Optional[str]): The language of the collection.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict[str, List[Dict[str, Any]]]: The structured metadata for the collection.
|
|
69
|
+
"""
|
|
70
|
+
return build_dso_metadata(title, description, abstract, rights, table_of_contents, language, **{
|
|
71
|
+
"dc.rights.license": license,
|
|
72
|
+
"dspace.entity.type": entity_type,
|
|
73
|
+
"cris.submission.definition": submission_definition,
|
|
74
|
+
"cris.submission.definition-correction": submission_definition_correction,
|
|
75
|
+
"cris.workspace.shared": workspace_shared,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CollectionManager(ContainerMixin, BaseManager):
|
|
80
|
+
"""Manages DSpace collections, their role groups, submission policies and settings"""
|
|
81
|
+
|
|
82
|
+
# The API endpoint and key for collections within DSpace
|
|
83
|
+
_CONTAINER_PATH = Endpoints.COLLECTIONS
|
|
84
|
+
_CONTAINER_KEY = "collections"
|
|
85
|
+
|
|
86
|
+
def __init__(self, auth):
|
|
87
|
+
"""Initialize the CollectionManager.
|
|
88
|
+
|
|
89
|
+
Parameters:
|
|
90
|
+
auth: The authentication object used to interact with the DSpace API.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
None
|
|
94
|
+
"""
|
|
95
|
+
logger.info("Initializing CollectionManager")
|
|
96
|
+
super().__init__(auth)
|
|
97
|
+
self._group_manager = None
|
|
98
|
+
self._permission_manager = None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def groups(self):
|
|
102
|
+
"""Get the group manager for collections.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
GroupManager: The group manager instance.
|
|
106
|
+
"""
|
|
107
|
+
if self._group_manager is None:
|
|
108
|
+
from .groups import GroupManager
|
|
109
|
+
self._group_manager = GroupManager(self.auth)
|
|
110
|
+
return self._group_manager
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def discovery(self):
|
|
114
|
+
"""Get the discovery manager for collections.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
DiscoveryManager: The discovery manager instance.
|
|
118
|
+
"""
|
|
119
|
+
if getattr(self, "_discovery_manager", None) is None:
|
|
120
|
+
from .discovery import DiscoveryManager
|
|
121
|
+
self._discovery_manager = DiscoveryManager(self.auth)
|
|
122
|
+
return self._discovery_manager
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def permissions(self):
|
|
126
|
+
"""Get the permission manager for collections.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
PermissionManager: The permission manager instance.
|
|
130
|
+
"""
|
|
131
|
+
if self._permission_manager is None:
|
|
132
|
+
from .permissions import PermissionManager
|
|
133
|
+
self._permission_manager = PermissionManager(self.auth)
|
|
134
|
+
return self._permission_manager
|
|
135
|
+
|
|
136
|
+
def get_collections(self, page=0, size=20, sort=None, all_languages=None, projection=None, embed=None):
|
|
137
|
+
"""
|
|
138
|
+
Get a list of collections
|
|
139
|
+
|
|
140
|
+
Parameters:
|
|
141
|
+
page (int): Page number (0-based)
|
|
142
|
+
size (int): Number of collections per page
|
|
143
|
+
sort (Optional[str]): Sort criteria
|
|
144
|
+
all_languages (Optional[bool]): Whether to include all languages
|
|
145
|
+
projection (Optional[str]): Projection parameter for the API
|
|
146
|
+
embed (Optional[str]): Embed parameter for the API
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
dict: API response containing collections
|
|
150
|
+
"""
|
|
151
|
+
logger.info("Getting collections (page=%d, size=%d)", page, size)
|
|
152
|
+
|
|
153
|
+
url = f"{self.config.dspace_url}{Endpoints.COLLECTIONS}"
|
|
154
|
+
params = {"page": page, "size": size}
|
|
155
|
+
if sort:
|
|
156
|
+
params["sort"] = sort
|
|
157
|
+
params = view_params(params, self.config, all_languages, projection, embed)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
response = self.session.get(
|
|
161
|
+
url,
|
|
162
|
+
headers=self.config.headers,
|
|
163
|
+
params=params,
|
|
164
|
+
timeout=self.config.timeout
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if response.status_code == 200:
|
|
168
|
+
logger.info("Successfully retrieved collections")
|
|
169
|
+
return response.json()
|
|
170
|
+
else:
|
|
171
|
+
handle_response_error(response, "Get collections")
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error("Failed to get collections: %s", e)
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
def get_collection(self, collection_uuid, all_languages=None, projection=None, embed=None):
|
|
178
|
+
"""
|
|
179
|
+
Get a specific collection by UUID
|
|
180
|
+
|
|
181
|
+
Parameters:
|
|
182
|
+
collection_uuid (str): UUID of the collection
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
dict: Collection data
|
|
186
|
+
"""
|
|
187
|
+
logger.info("Getting collection %s", collection_uuid)
|
|
188
|
+
|
|
189
|
+
url = f"{self.config.dspace_url}{Endpoints.COLLECTIONS}/{collection_uuid}"
|
|
190
|
+
params = view_params(None, self.config, all_languages, projection, embed)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
response = self.session.get(
|
|
194
|
+
url,
|
|
195
|
+
headers=self.config.headers,
|
|
196
|
+
params=params,
|
|
197
|
+
timeout=self.config.timeout
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if response.status_code == 200:
|
|
201
|
+
logger.info("Successfully retrieved collection %s", collection_uuid)
|
|
202
|
+
return response.json()
|
|
203
|
+
handle_response_error(response, "Get collection %s" % collection_uuid)
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error("Failed to get collection %s: %s", collection_uuid, e)
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
def get_collection_items(self, collection_uuid, page=0, size=20, sort=None, all_languages=None, projection=None,
|
|
210
|
+
embed=None, query=None, filters=None):
|
|
211
|
+
"""
|
|
212
|
+
One page of the items in a collection.
|
|
213
|
+
|
|
214
|
+
DSpace 7+ has no /core/collections/{uuid}/items endpoint; like the
|
|
215
|
+
Angular UI, this uses a discovery search scoped to the collection
|
|
216
|
+
(dsoType=item). The result keeps the familiar shape:
|
|
217
|
+
|
|
218
|
+
{"_embedded": {"items": [item, ...]}, "page": {"number", "size", "totalElements", "totalPages"}}
|
|
219
|
+
|
|
220
|
+
Parameters:
|
|
221
|
+
collection_uuid: UUID of the collection
|
|
222
|
+
page (int): 0-based page number
|
|
223
|
+
size (int): Page size
|
|
224
|
+
sort: discovery sort option, e.g. "dc.title,ASC", "dc.date.accessioned,DESC"
|
|
225
|
+
(see DiscoveryManager.get_sort_options())
|
|
226
|
+
query (Optional[str]): Optional discovery query
|
|
227
|
+
filters (Optional[dict]): Optional discovery filters (see DiscoveryManager.search)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
dict: A dictionary containing the items and pagination information.
|
|
231
|
+
|
|
232
|
+
Notes:
|
|
233
|
+
- Items mapped into the collection are included (discovery scope covers them).
|
|
234
|
+
- Discovery shows what the current user may see: withdrawn and
|
|
235
|
+
private (non-discoverable) items only appear for administrators.
|
|
236
|
+
- Newly created items appear once Solr has indexed them.
|
|
237
|
+
"""
|
|
238
|
+
logger.info("Getting items for collection %s (page %s)", collection_uuid, page)
|
|
239
|
+
response = self.discovery.search(query=query, dso_type="item", scope=collection_uuid, filters=filters,
|
|
240
|
+
sort=sort, page=page, size=size, all_languages=all_languages,
|
|
241
|
+
projection=projection, embed=embed)
|
|
242
|
+
objects, info = self.discovery.search_result(response)
|
|
243
|
+
items = [(o.get("_embedded") or {}).get("indexableObject") for o in objects]
|
|
244
|
+
return {"_embedded": {"items": [i for i in items if i]},
|
|
245
|
+
"page": {"number": info.get("number", page), "size": info.get("size", size),
|
|
246
|
+
"totalElements": info.get("totalElements", 0),
|
|
247
|
+
"totalPages": info.get("totalPages", 0)}}
|
|
248
|
+
|
|
249
|
+
def iter_collection_items(self, collection_uuid, page_size=100, max_results=None, sort=None, query=None,
|
|
250
|
+
filters=None, all_languages=None, projection=None, embed=None):
|
|
251
|
+
"""Every item in a collection, across all pages (a generator). See get_collection_items().
|
|
252
|
+
|
|
253
|
+
Parameters:
|
|
254
|
+
collection_uuid (str): UUID of the collection
|
|
255
|
+
page_size (int): Number of items per page
|
|
256
|
+
max_results (Optional[int]): Maximum number of items to return
|
|
257
|
+
sort (Optional[str]): Sort criteria
|
|
258
|
+
query (Optional[str]): Optional discovery query
|
|
259
|
+
filters (Optional[dict]): Optional discovery filters
|
|
260
|
+
all_languages (Optional[bool]): Whether to include all languages
|
|
261
|
+
projection (Optional[str]): Projection parameter for the API
|
|
262
|
+
embed (Optional[str]): Embed parameter for the API
|
|
263
|
+
|
|
264
|
+
Yields:
|
|
265
|
+
dict: Items in the collection
|
|
266
|
+
"""
|
|
267
|
+
return self.discovery.iter_search(query=query, dso_type="item", scope=collection_uuid, filters=filters,
|
|
268
|
+
sort=sort, page_size=page_size, max_results=max_results,
|
|
269
|
+
all_languages=all_languages, projection=projection, embed=embed)
|
|
270
|
+
|
|
271
|
+
def count_collection_items(self, collection_uuid, query=None, filters=None):
|
|
272
|
+
"""Number of items in a collection visible to the current user (one request).
|
|
273
|
+
|
|
274
|
+
Parameters:
|
|
275
|
+
collection_uuid (str): UUID of the collection
|
|
276
|
+
query (Optional[str]): Optional discovery query
|
|
277
|
+
filters (Optional[dict]): Optional discovery filters
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
int: Number of items in the collection
|
|
281
|
+
"""
|
|
282
|
+
return self.discovery.count(query=query, dso_type="item", scope=collection_uuid, filters=filters)
|
|
283
|
+
|
|
284
|
+
def create_collection(self, parent_community_uuid, name, metadata=None):
|
|
285
|
+
"""
|
|
286
|
+
Create a new collection.
|
|
287
|
+
|
|
288
|
+
This uses POST /core/collections?parent={communityUUID}
|
|
289
|
+
|
|
290
|
+
Parameters:
|
|
291
|
+
parent_community_uuid (str): UUID of the parent community
|
|
292
|
+
name (str): Friendly name for the collection
|
|
293
|
+
metadata (Optional[dict]): Optional metadata dict. If 'dc.title' is missing, it will be set from `name`.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
dict: Created collection data
|
|
297
|
+
"""
|
|
298
|
+
logger.info("Creating collection '%s' in community %s", name, parent_community_uuid)
|
|
299
|
+
|
|
300
|
+
# Ensure metadata exists and contains dc.title from name if not supplied
|
|
301
|
+
metadata = dict(metadata or {})
|
|
302
|
+
if "dc.title" not in metadata:
|
|
303
|
+
metadata["dc.title"] = [{"value": name}]
|
|
304
|
+
else:
|
|
305
|
+
# normalize metadata field variants via formatter
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
collection_data = {
|
|
309
|
+
# Some DSpace installations accept/require this top-level name as well:
|
|
310
|
+
"name": name,
|
|
311
|
+
"metadata": self._format_metadata(metadata),
|
|
312
|
+
"type": "collection",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
url = f"{self.config.dspace_url}{Endpoints.COLLECTIONS}?parent={parent_community_uuid}"
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
response = self.session.post(
|
|
319
|
+
url,
|
|
320
|
+
headers=self.config.headers,
|
|
321
|
+
json=collection_data,
|
|
322
|
+
timeout=self.config.timeout
|
|
323
|
+
)
|
|
324
|
+
if response.status_code in (200, 201):
|
|
325
|
+
logger.info("Collection created successfully")
|
|
326
|
+
return response.json()
|
|
327
|
+
handle_response_error(response, "Create collection")
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.error("Failed to create collection: %s", e)
|
|
330
|
+
raise
|
|
331
|
+
|
|
332
|
+
def update_collection(self, collection_uuid, name=None, metadata=None):
|
|
333
|
+
"""
|
|
334
|
+
Update collection metadata.
|
|
335
|
+
|
|
336
|
+
Parameters:
|
|
337
|
+
collection_uuid (str): UUID of the collection
|
|
338
|
+
name (Optional[str]): New name for the collection (maps to metadata 'dc.title')
|
|
339
|
+
metadata (Optional[dict]): Dict of metadata fields to replace. NOTE: 'dspace.entity.type' will be ignored.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
dict: Updated collection data
|
|
343
|
+
"""
|
|
344
|
+
logger.info("Updating collection %s", collection_uuid)
|
|
345
|
+
|
|
346
|
+
operations = []
|
|
347
|
+
|
|
348
|
+
# If a name is provided, update the dc.title field instead of /name
|
|
349
|
+
if name:
|
|
350
|
+
operations.append({
|
|
351
|
+
"op": "replace",
|
|
352
|
+
"path": "/metadata/dc.title",
|
|
353
|
+
"value": {"value": name}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
if metadata:
|
|
357
|
+
# NEVER allow changing dspace.entity.type via this method
|
|
358
|
+
safe_metadata = {
|
|
359
|
+
k: v for k, v in metadata.items()
|
|
360
|
+
if k != "dspace.entity.type"
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for field, values in safe_metadata.items():
|
|
364
|
+
# If caller passed a simple string or boolean, wrap once
|
|
365
|
+
if isinstance(values, (str, bool, int, float)):
|
|
366
|
+
value_obj = {"value": values}
|
|
367
|
+
operations.append({
|
|
368
|
+
"op": "replace",
|
|
369
|
+
"path": f"/metadata/{field}",
|
|
370
|
+
"value": value_obj
|
|
371
|
+
})
|
|
372
|
+
elif isinstance(values, list):
|
|
373
|
+
# If they passed a list of primitives/objects, normalize to list of {"value": ...}
|
|
374
|
+
normalized = []
|
|
375
|
+
for v in values:
|
|
376
|
+
if isinstance(v, dict) and "value" in v:
|
|
377
|
+
normalized.append(v)
|
|
378
|
+
else:
|
|
379
|
+
normalized.append({"value": v})
|
|
380
|
+
operations.append({
|
|
381
|
+
"op": "replace",
|
|
382
|
+
"path": f"/metadata/{field}",
|
|
383
|
+
"value": normalized if len(normalized) > 1 else normalized[0]
|
|
384
|
+
})
|
|
385
|
+
elif isinstance(values, dict):
|
|
386
|
+
# Assume already a MetadataValue-like object
|
|
387
|
+
operations.append({
|
|
388
|
+
"op": "replace",
|
|
389
|
+
"path": f"/metadata/{field}",
|
|
390
|
+
"value": values
|
|
391
|
+
})
|
|
392
|
+
else:
|
|
393
|
+
operations.append({
|
|
394
|
+
"op": "replace",
|
|
395
|
+
"path": f"/metadata/{field}",
|
|
396
|
+
"value": {"value": str(values)}
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
url = f"{self.config.dspace_url}{Endpoints.COLLECTIONS}/{collection_uuid}"
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
headers = dict(self.config.headers)
|
|
403
|
+
headers["Content-Type"] = "application/json-patch+json"
|
|
404
|
+
|
|
405
|
+
response = self.session.patch(
|
|
406
|
+
url,
|
|
407
|
+
headers=headers,
|
|
408
|
+
json=operations,
|
|
409
|
+
timeout=self.config.timeout
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if response.status_code == 200:
|
|
413
|
+
logger.info("Collection %s updated successfully", collection_uuid)
|
|
414
|
+
return response.json()
|
|
415
|
+
|
|
416
|
+
if response.status_code == 204:
|
|
417
|
+
logger.info("Collection %s updated successfully (204). Fetching resource...", collection_uuid)
|
|
418
|
+
return self.get_collection(collection_uuid)
|
|
419
|
+
|
|
420
|
+
handle_response_error(response, f"Update collection {collection_uuid}")
|
|
421
|
+
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.error("Failed to update collection %s: %s", collection_uuid, e)
|
|
424
|
+
raise
|
|
425
|
+
|
|
426
|
+
def delete_collection(self, collection_uuid):
|
|
427
|
+
"""
|
|
428
|
+
Delete a collection
|
|
429
|
+
|
|
430
|
+
Parameters:
|
|
431
|
+
collection_uuid (str): UUID of the collection to delete
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
bool: True if deletion was successful
|
|
435
|
+
"""
|
|
436
|
+
logger.info("Deleting collection %s", collection_uuid)
|
|
437
|
+
|
|
438
|
+
url = f"{self.config.dspace_url}{Endpoints.COLLECTIONS}/{collection_uuid}"
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
response = self.session.delete(
|
|
442
|
+
url,
|
|
443
|
+
headers=self.config.headers,
|
|
444
|
+
timeout=self.config.timeout
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if response.status_code == 204:
|
|
448
|
+
logger.info("Collection %s deleted successfully", collection_uuid)
|
|
449
|
+
return True
|
|
450
|
+
handle_response_error(response, f"Delete collection {collection_uuid}")
|
|
451
|
+
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.error("Failed to delete collection %s: %s", collection_uuid, e)
|
|
454
|
+
raise
|
|
455
|
+
|
|
456
|
+
def _format_metadata(self, metadata_dict):
|
|
457
|
+
"""
|
|
458
|
+
Format metadata dictionary for DSpace API
|
|
459
|
+
|
|
460
|
+
Parameters:
|
|
461
|
+
metadata_dict (dict): Dictionary of metadata fields
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
dict: Formatted metadata
|
|
465
|
+
"""
|
|
466
|
+
formatted = {}
|
|
467
|
+
|
|
468
|
+
for field, values in metadata_dict.items():
|
|
469
|
+
formatted[field] = self._format_metadata_field(values)
|
|
470
|
+
|
|
471
|
+
return formatted
|
|
472
|
+
|
|
473
|
+
def _format_metadata_field(self, values):
|
|
474
|
+
"""
|
|
475
|
+
Format metadata field values for DSpace API
|
|
476
|
+
|
|
477
|
+
Parameters:
|
|
478
|
+
values (Union[str, List[str], List[dict]]): String, list of strings, or list of dicts
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
list: Formatted metadata values
|
|
482
|
+
"""
|
|
483
|
+
if isinstance(values, str):
|
|
484
|
+
return [{"value": values}]
|
|
485
|
+
elif isinstance(values, list):
|
|
486
|
+
formatted = []
|
|
487
|
+
for value in values:
|
|
488
|
+
if isinstance(value, str):
|
|
489
|
+
formatted.append({"value": value})
|
|
490
|
+
elif isinstance(value, dict):
|
|
491
|
+
formatted.append(value)
|
|
492
|
+
else:
|
|
493
|
+
formatted.append({"value": str(value)})
|
|
494
|
+
return formatted
|
|
495
|
+
else:
|
|
496
|
+
return [{"value": str(values)}]
|
|
497
|
+
|
|
498
|
+
# ------------------------------------------------------------------
|
|
499
|
+
# Finding collections
|
|
500
|
+
# ------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
def search_editable_collections(self, query: str = "", configuration: str = "editCollection",
|
|
503
|
+
max_results: Optional[int] = 20,
|
|
504
|
+
all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
505
|
+
"""
|
|
506
|
+
Collections the current user can edit (discovery configuration).
|
|
507
|
+
If your discovery.xml names the configuration differently, pass it.
|
|
508
|
+
|
|
509
|
+
Parameters:
|
|
510
|
+
query (str): Discovery query string
|
|
511
|
+
configuration (str): Discovery configuration name
|
|
512
|
+
max_results (Optional[int]): Maximum number of results to return
|
|
513
|
+
all_languages (Optional[bool]): Whether to include all languages
|
|
514
|
+
projection (ProjectionArg): Projection argument for the API
|
|
515
|
+
embed (EmbedArg): Embed argument for the API
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
List[Dict[str, Any]]: List of collections the current user can edit
|
|
519
|
+
"""
|
|
520
|
+
return self._discover(query, dso_type="COLLECTION", configuration=configuration, sort="dc.title,ASC",
|
|
521
|
+
max_results=max_results, all_languages=all_languages, projection=projection, embed=embed)
|
|
522
|
+
|
|
523
|
+
def find_submit_authorized(self, query: Optional[str] = None, entity_type: Optional[str] = None,
|
|
524
|
+
max_results: Optional[int] = None,
|
|
525
|
+
all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None,
|
|
526
|
+
community_uuid: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
527
|
+
"""Collections the current user may submit to, optionally of one entity type and/or below one community.
|
|
528
|
+
|
|
529
|
+
Parameters:
|
|
530
|
+
query (Optional[str]): Optional discovery query
|
|
531
|
+
entity_type (Optional[str]): Optional entity type filter
|
|
532
|
+
max_results (Optional[int]): Maximum number of results to return
|
|
533
|
+
all_languages (Optional[bool]): Whether to include all languages
|
|
534
|
+
projection (ProjectionArg): Projection argument for the API
|
|
535
|
+
embed (EmbedArg): Embed argument for the API
|
|
536
|
+
community_uuid (Optional[str]): UUID of the community to filter by
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
List[Dict[str, Any]]: List of collections the current user may submit to
|
|
540
|
+
"""
|
|
541
|
+
method = "findSubmitAuthorized" + ("ByCommunity" if community_uuid else "") + \
|
|
542
|
+
(("AndEntityType" if community_uuid else "ByEntityType") if entity_type else "")
|
|
543
|
+
url = self._url(Endpoints.COLLECTIONS, f"/search/{method}")
|
|
544
|
+
params = [("uuid", community_uuid)] if community_uuid else []
|
|
545
|
+
if entity_type:
|
|
546
|
+
params.append(("entityType", entity_type))
|
|
547
|
+
if query:
|
|
548
|
+
params.append(("query", query))
|
|
549
|
+
return list(self._iter_embedded(url, "collections", "Find submit-authorized collections", params,
|
|
550
|
+
max_results=max_results, all_languages=all_languages, projection=projection, embed=embed))
|
|
551
|
+
|
|
552
|
+
# ------------------------------------------------------------------
|
|
553
|
+
# Role groups: admin, submitters, default item/bitstream read, workflow
|
|
554
|
+
# ------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
def get_role_group(self, collection_uuid: str, role: str) -> Optional[Dict[str, Any]]:
|
|
557
|
+
"""Get the role group for a collection.
|
|
558
|
+
|
|
559
|
+
Parameters:
|
|
560
|
+
collection_uuid (str): UUID of the collection
|
|
561
|
+
role (str): Role name (adminGroup, submittersGroup, itemReadGroup, bitstreamReadGroup or workflowGroups/<name>)
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Optional[Dict[str, Any]]: Role group information if it exists, otherwise None.
|
|
565
|
+
"""
|
|
566
|
+
return self.groups.get_role_group("collection", collection_uuid, role)
|
|
567
|
+
|
|
568
|
+
def get_or_create_role_group(self, collection_uuid: str, role: str,
|
|
569
|
+
description: Optional[str] = None) -> Dict[str, Any]:
|
|
570
|
+
"""Get or create the role group for a collection.
|
|
571
|
+
|
|
572
|
+
Parameters:
|
|
573
|
+
collection_uuid (str): UUID of the collection
|
|
574
|
+
role (str): Role name (adminGroup, submittersGroup, itemReadGroup, bitstreamReadGroup or workflowGroups/<name>)
|
|
575
|
+
description (Optional[str]): Description for the role group
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
Dict[str, Any]: Role group information.
|
|
579
|
+
"""
|
|
580
|
+
return self.groups.get_or_create_role_group("collection", collection_uuid, role, description)
|
|
581
|
+
|
|
582
|
+
def delete_role_group(self, collection_uuid: str, role: str) -> bool:
|
|
583
|
+
"""Delete the role group for a collection.
|
|
584
|
+
|
|
585
|
+
Parameters:
|
|
586
|
+
collection_uuid (str): UUID of the collection
|
|
587
|
+
role (str): Role name
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
bool: True if the role group was deleted, False otherwise.
|
|
591
|
+
"""
|
|
592
|
+
return self.groups.delete_role_group("collection", collection_uuid, role)
|
|
593
|
+
|
|
594
|
+
def add_role_members(self, collection_uuid: str, role: str, epersons: Iterable[Ref] = (),
|
|
595
|
+
subgroups: Iterable[Ref] = (), description: Optional[str] = None) -> Dict[str, Any]:
|
|
596
|
+
"""Create the role group if needed and add epersons and/or groups to it.
|
|
597
|
+
|
|
598
|
+
Parameters:
|
|
599
|
+
collection_uuid (str): UUID of the collection
|
|
600
|
+
role (str): Role name
|
|
601
|
+
epersons (Iterable[Ref]): Epersons to add to the role group
|
|
602
|
+
subgroups (Iterable[Ref]): Subgroups to add to the role group
|
|
603
|
+
description (Optional[str]): Description for the role group
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Dict[str, Any]: Role group information.
|
|
607
|
+
"""
|
|
608
|
+
return self.groups.ensure_role_members("collection", collection_uuid, role, epersons, subgroups, description)
|
|
609
|
+
|
|
610
|
+
def get_role_members(self, collection_uuid: str, role: str) -> Dict[str, List[Dict[str, Any]]]:
|
|
611
|
+
"""Get the members of a role group.
|
|
612
|
+
|
|
613
|
+
Parameters:
|
|
614
|
+
collection_uuid (str): UUID of the collection
|
|
615
|
+
role (str): Role name
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
Dict[str, List[Dict[str, Any]]]: {"epersons": [...], "subgroups": [...]}; both empty if the role group doesn't exist.
|
|
619
|
+
"""
|
|
620
|
+
group = self.get_role_group(collection_uuid, role)
|
|
621
|
+
if not group:
|
|
622
|
+
return {"epersons": [], "subgroups": []}
|
|
623
|
+
return {"epersons": self.groups.get_group_epersons(group["uuid"]),
|
|
624
|
+
"subgroups": self.groups.get_subgroups(group["uuid"])}
|
|
625
|
+
|
|
626
|
+
def add_admins(self, collection_uuid: str, epersons: Iterable[Ref] = (), subgroups: Iterable[Ref] = ()):
|
|
627
|
+
"""Add epersons and/or subgroups to the admin role group.
|
|
628
|
+
|
|
629
|
+
Parameters:
|
|
630
|
+
collection_uuid (str): UUID of the collection
|
|
631
|
+
epersons (Iterable[Ref]): Epersons to add to the role group
|
|
632
|
+
subgroups (Iterable[Ref]): Subgroups to add to the role group
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
Dict[str, Any]: Role group information.
|
|
636
|
+
"""
|
|
637
|
+
return self.add_role_members(collection_uuid, ADMIN, epersons, subgroups)
|
|
638
|
+
|
|
639
|
+
def add_submitters(self, collection_uuid: str, epersons: Iterable[Ref] = (), subgroups: Iterable[Ref] = ()):
|
|
640
|
+
"""Add epersons and/or subgroups to the submitters role group.
|
|
641
|
+
|
|
642
|
+
Parameters:
|
|
643
|
+
collection_uuid (str): UUID of the collection
|
|
644
|
+
epersons (Iterable[Ref]): Epersons to add to the role group
|
|
645
|
+
subgroups (Iterable[Ref]): Subgroups to add to the role group
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Dict[str, Any]: Role group information.
|
|
649
|
+
"""
|
|
650
|
+
return self.add_role_members(collection_uuid, SUBMITTERS, epersons, subgroups)
|
|
651
|
+
|
|
652
|
+
def restrict_item_read(self, collection_uuid: str, epersons: Iterable[Ref] = (), subgroups: Iterable[Ref] = (),
|
|
653
|
+
description: Optional[str] = None) -> Dict[str, Any]:
|
|
654
|
+
"""Make items deposited from now on readable only by the given members
|
|
655
|
+
(creates itemReadGroup, which replaces Anonymous as default reader).
|
|
656
|
+
Items already in the collection keep their policies - use
|
|
657
|
+
PermissionManager.remove_group_access_from_item() for those.
|
|
658
|
+
|
|
659
|
+
Parameters:
|
|
660
|
+
collection_uuid (str): UUID of the collection
|
|
661
|
+
epersons (Iterable[Ref]): Epersons to add to the role group
|
|
662
|
+
subgroups (Iterable[Ref]): Subgroups to add to the role group
|
|
663
|
+
description (Optional[str]): Description for the role group
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
Dict[str, Any]: Role group information.
|
|
667
|
+
"""
|
|
668
|
+
return self.add_role_members(collection_uuid, ITEM_READ, epersons, subgroups, description)
|
|
669
|
+
|
|
670
|
+
def restrict_bitstream_read(self, collection_uuid: str, epersons: Iterable[Ref] = (),
|
|
671
|
+
subgroups: Iterable[Ref] = (), description: Optional[str] = None) -> Dict[str, Any]:
|
|
672
|
+
"""Same as restrict_item_read() for the files (bitstreams) of new items.
|
|
673
|
+
|
|
674
|
+
Parameters:
|
|
675
|
+
collection_uuid (str): UUID of the collection
|
|
676
|
+
epersons (Iterable[Ref]): Epersons to add to the role group
|
|
677
|
+
subgroups (Iterable[Ref]): Subgroups to add to the role group
|
|
678
|
+
description (Optional[str]): Description for the role group
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
Dict[str, Any]: Role group information.
|
|
682
|
+
"""
|
|
683
|
+
return self.add_role_members(collection_uuid, BITSTREAM_READ, epersons, subgroups, description)
|
|
684
|
+
|
|
685
|
+
def is_read_restricted(self, collection_uuid: str) -> Dict[str, bool]:
|
|
686
|
+
"""Check if new items / new bitstreams are publicly readable, based on the
|
|
687
|
+
collection's DEFAULT_ITEM_READ / DEFAULT_BITSTREAM_READ policies.
|
|
688
|
+
|
|
689
|
+
Parameters:
|
|
690
|
+
collection_uuid (str): UUID of the collection
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
Dict[str, bool]: Dictionary with keys "items" and "bitstreams" indicating readability.
|
|
694
|
+
"""
|
|
695
|
+
from .permissions import ANONYMOUS, Actions
|
|
696
|
+
policies = self.permissions.get_resource_policies(collection_uuid)
|
|
697
|
+
result = {}
|
|
698
|
+
for key, action in (("items", Actions.DEFAULT_ITEM_READ), ("bitstreams", Actions.DEFAULT_BITSTREAM_READ)):
|
|
699
|
+
result[key] = not self.permissions.filter_policies(policies, group=ANONYMOUS, actions=[action])
|
|
700
|
+
return result
|
|
701
|
+
|
|
702
|
+
# ------------------------------------------------------------------
|
|
703
|
+
# Submission policies (TYPE_SUBMISSION resource policies on the collection)
|
|
704
|
+
# ------------------------------------------------------------------
|
|
705
|
+
|
|
706
|
+
def get_submission_policies(self, collection_uuid: str) -> List[Dict[str, Any]]:
|
|
707
|
+
"""Get all TYPE_SUBMISSION policies for the collection.
|
|
708
|
+
|
|
709
|
+
Parameters:
|
|
710
|
+
collection_uuid (str): UUID of the collection
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
List[Dict[str, Any]]: List of submission policies.
|
|
714
|
+
"""
|
|
715
|
+
from .permissions import PolicyTypes
|
|
716
|
+
policies = self.permissions.get_resource_policies(collection_uuid)
|
|
717
|
+
return self.permissions.filter_policies(policies, policy_types=[PolicyTypes.SUBMISSION])
|
|
718
|
+
|
|
719
|
+
def add_submission_policy(self, collection_uuid: str, group: Ref, action: str = "ADD",
|
|
720
|
+
name: Optional[str] = None, start_date=None, end_date=None) -> Dict[str, Any]:
|
|
721
|
+
"""Grant a group an action on the collection as a TYPE_SUBMISSION policy (admin only).
|
|
722
|
+
|
|
723
|
+
Parameters:
|
|
724
|
+
collection_uuid (str): UUID of the collection
|
|
725
|
+
group (Ref): Group to grant the action to
|
|
726
|
+
action (str): Action to grant (default "ADD")
|
|
727
|
+
name (Optional[str]): Name of the policy
|
|
728
|
+
start_date (Optional): Start date of the policy
|
|
729
|
+
end_date (Optional): End date of the policy
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
Dict[str, Any]: Policy information.
|
|
733
|
+
"""
|
|
734
|
+
from .permissions import PolicyTypes
|
|
735
|
+
group_uuid = self.groups.resolve_group(group)
|
|
736
|
+
return self.permissions.create_policy(collection_uuid, action=action, group=group_uuid,
|
|
737
|
+
policy_type=PolicyTypes.SUBMISSION, name=name,
|
|
738
|
+
start_date=start_date, end_date=end_date)
|
|
739
|
+
|
|
740
|
+
def delete_submission_policy(self, policy_id: int) -> bool:
|
|
741
|
+
"""Delete a TYPE_SUBMISSION policy by its ID.
|
|
742
|
+
|
|
743
|
+
Parameters:
|
|
744
|
+
policy_id (int): ID of the policy to delete
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
bool: True if the policy was successfully deleted, False otherwise.
|
|
748
|
+
"""
|
|
749
|
+
return self.permissions.delete_policy(policy_id)
|
|
750
|
+
|
|
751
|
+
# ------------------------------------------------------------------
|
|
752
|
+
# License
|
|
753
|
+
# ------------------------------------------------------------------
|
|
754
|
+
|
|
755
|
+
def get_license(self, collection_uuid: str) -> Dict[str, Any]:
|
|
756
|
+
"""
|
|
757
|
+
Get the license of the collection.
|
|
758
|
+
|
|
759
|
+
{"custom": bool, "text": ...} - the collection's license or the site default.
|
|
760
|
+
|
|
761
|
+
Parameters:
|
|
762
|
+
collection_uuid (str): UUID of the collection
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
Dict[str, Any]: License information.
|
|
766
|
+
"""
|
|
767
|
+
return self._get_json(self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}/license"),
|
|
768
|
+
f"Get license of collection {collection_uuid}")
|
|
769
|
+
|
|
770
|
+
def set_license(self, collection_uuid: str, text: Optional[str]) -> Dict[str, Any]:
|
|
771
|
+
"""Set a custom deposit license (dc.rights.license); text=None returns to the site default.
|
|
772
|
+
|
|
773
|
+
Parameters:
|
|
774
|
+
collection_uuid (str): UUID of the collection
|
|
775
|
+
text (Optional[str]): License text to set, or None to revert to the site default
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
Dict[str, Any]: License information.
|
|
779
|
+
"""
|
|
780
|
+
patch = MetadataPatch()
|
|
781
|
+
if text is None:
|
|
782
|
+
current = self.get_collection(collection_uuid, all_languages=True) or {}
|
|
783
|
+
if not (current.get("metadata") or {}).get("dc.rights.license"):
|
|
784
|
+
return current
|
|
785
|
+
patch.remove("dc.rights.license")
|
|
786
|
+
else:
|
|
787
|
+
patch.set("dc.rights.license", text)
|
|
788
|
+
return self.patch_metadata(collection_uuid, patch)
|
|
789
|
+
|
|
790
|
+
# ------------------------------------------------------------------
|
|
791
|
+
# Item template
|
|
792
|
+
# ------------------------------------------------------------------
|
|
793
|
+
|
|
794
|
+
def get_item_template(self, collection_uuid: str, all_languages: Optional[bool] = None) -> Optional[Dict[str, Any]]:
|
|
795
|
+
"""
|
|
796
|
+
The template item (metadata copied into new submissions), or None.
|
|
797
|
+
DSpace answers 404 both for "no template" and for an unknown
|
|
798
|
+
collection; the second case still raises NotFoundError.
|
|
799
|
+
|
|
800
|
+
Parameters:
|
|
801
|
+
collection_uuid (str): UUID of the collection
|
|
802
|
+
all_languages (Optional[bool]): Whether to retrieve metadata in all languages
|
|
803
|
+
|
|
804
|
+
Returns:
|
|
805
|
+
Optional[Dict[str, Any]]: Item template metadata, or None if no template exists.
|
|
806
|
+
"""
|
|
807
|
+
try:
|
|
808
|
+
return self._get_optional(self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}/itemtemplate"),
|
|
809
|
+
f"Get item template of collection {collection_uuid}",
|
|
810
|
+
all_languages=all_languages)
|
|
811
|
+
except NotFoundError:
|
|
812
|
+
# raises NotFoundError itself if the collection does not exist
|
|
813
|
+
self._get_json(self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}"),
|
|
814
|
+
f"Get collection {collection_uuid}", all_languages=False)
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
def create_item_template(self, collection_uuid: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
818
|
+
"""Create an item template for the collection.
|
|
819
|
+
|
|
820
|
+
Parameters:
|
|
821
|
+
collection_uuid (str): UUID of the collection
|
|
822
|
+
metadata (Dict[str, Any]): Metadata for the item template
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
Dict[str, Any]: Created item template
|
|
826
|
+
|
|
827
|
+
Raises:
|
|
828
|
+
ValueError: If the collection already has a template (HTTP 422)
|
|
829
|
+
"""
|
|
830
|
+
body = {"metadata": self._format_metadata(metadata), "inArchive": False, "discoverable": False,
|
|
831
|
+
"withdrawn": False, "type": "item"}
|
|
832
|
+
template = self._request("POST", self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}/itemtemplate"),
|
|
833
|
+
f"Create item template of collection {collection_uuid}", expected=(200, 201),
|
|
834
|
+
headers=self._headers(), json=body).json()
|
|
835
|
+
logger.info("Created item template %s for collection %s", template.get("uuid"), collection_uuid)
|
|
836
|
+
return template
|
|
837
|
+
|
|
838
|
+
def update_item_template(self, template_uuid: str, patch: Union[MetadataPatch, Dict[str, Any]]) -> Dict[str, Any]:
|
|
839
|
+
"""Update an item template with the given metadata patch.
|
|
840
|
+
|
|
841
|
+
Parameters:
|
|
842
|
+
template_uuid (str): UUID of the item template
|
|
843
|
+
patch (Union[MetadataPatch, Dict[str, Any]]): Metadata patch to apply. Can be a MetadataPatch instance or a dictionary {field: values} where each replaces the field.
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
Dict[str, Any]: Updated item template
|
|
847
|
+
|
|
848
|
+
Raises:
|
|
849
|
+
ValueError: If the patch is empty
|
|
850
|
+
"""
|
|
851
|
+
if isinstance(patch, dict):
|
|
852
|
+
built = MetadataPatch()
|
|
853
|
+
for field, values in patch.items():
|
|
854
|
+
built.set(field, values)
|
|
855
|
+
patch = built
|
|
856
|
+
if not len(patch):
|
|
857
|
+
raise ValueError("Empty metadata patch")
|
|
858
|
+
return self._request("PATCH", self._url("/core/itemtemplates", f"/{template_uuid}"),
|
|
859
|
+
f"Update item template {template_uuid}", headers=self._headers(),
|
|
860
|
+
json=patch.operations).json()
|
|
861
|
+
|
|
862
|
+
def set_item_template(self, collection_uuid: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
863
|
+
"""Create the template, or set these fields on the existing one (other fields stay).
|
|
864
|
+
|
|
865
|
+
Parameters:
|
|
866
|
+
collection_uuid (str): UUID of the collection
|
|
867
|
+
metadata (Dict[str, Any]): Metadata to set on the item template
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
Dict[str, Any]: Created or updated item template
|
|
871
|
+
"""
|
|
872
|
+
template = self.get_item_template(collection_uuid)
|
|
873
|
+
if template is None:
|
|
874
|
+
return self.create_item_template(collection_uuid, metadata)
|
|
875
|
+
return self.update_item_template(template["uuid"], metadata)
|
|
876
|
+
|
|
877
|
+
def delete_item_template(self, collection_uuid: str) -> bool:
|
|
878
|
+
"""Remove the collection's template; False if it has none.
|
|
879
|
+
|
|
880
|
+
Parameters:
|
|
881
|
+
collection_uuid (str): UUID of the collection
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
bool: True if the template was deleted, False if it had none.
|
|
885
|
+
"""
|
|
886
|
+
template = self.get_item_template(collection_uuid)
|
|
887
|
+
if template is None:
|
|
888
|
+
return False
|
|
889
|
+
self._request("DELETE", self._url("/core/itemtemplates", f"/{template['uuid']}"),
|
|
890
|
+
f"Delete item template {template['uuid']}", expected=(204,))
|
|
891
|
+
return True
|
|
892
|
+
|
|
893
|
+
# ------------------------------------------------------------------
|
|
894
|
+
# Default access conditions
|
|
895
|
+
# ------------------------------------------------------------------
|
|
896
|
+
|
|
897
|
+
def get_default_access_conditions(self, collection_uuid: str, embed: EmbedArg = "group") -> List[Dict[str, Any]]:
|
|
898
|
+
"""
|
|
899
|
+
The DEFAULT_BITSTREAM_READ policies new files inherit (a resource
|
|
900
|
+
policy search; DSpace 7+ has no /defaultAccessConditions endpoint).
|
|
901
|
+
|
|
902
|
+
Parameters:
|
|
903
|
+
collection_uuid (str): UUID of the collection
|
|
904
|
+
embed (EmbedArg): Embedding options for the response
|
|
905
|
+
|
|
906
|
+
Returns:
|
|
907
|
+
List[Dict[str, Any]]: List of default access condition policies
|
|
908
|
+
"""
|
|
909
|
+
from .permissions import Actions
|
|
910
|
+
embeds = [embed] if isinstance(embed, str) else list(embed or [])
|
|
911
|
+
return self.permissions.get_resource_policies(collection_uuid, action=Actions.DEFAULT_BITSTREAM_READ,
|
|
912
|
+
embed=embeds)
|
|
913
|
+
|
|
914
|
+
# ------------------------------------------------------------------
|
|
915
|
+
# Mapped items
|
|
916
|
+
# ------------------------------------------------------------------
|
|
917
|
+
|
|
918
|
+
def iter_mapped_items(self, collection_uuid: str, all_languages: Optional[bool] = None,
|
|
919
|
+
embed: EmbedArg = None):
|
|
920
|
+
"""Items mapped into (not owned by) the collection. A generator.
|
|
921
|
+
|
|
922
|
+
Parameters:
|
|
923
|
+
collection_uuid (str): UUID of the collection
|
|
924
|
+
all_languages (Optional[bool]): Whether to retrieve metadata in all languages
|
|
925
|
+
embed (EmbedArg): Embedding options for the response
|
|
926
|
+
|
|
927
|
+
Yields:
|
|
928
|
+
Dict[str, Any]: Mapped item metadata
|
|
929
|
+
"""
|
|
930
|
+
url = self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}/mappedItems")
|
|
931
|
+
return self._iter_embedded(url, ("mappedItems", "items"), f"Get mapped items of {collection_uuid}",
|
|
932
|
+
all_languages=all_languages, embed=embed)
|
|
933
|
+
|
|
934
|
+
def get_mapped_items(self, collection_uuid: str, all_languages: Optional[bool] = None) -> List[Dict[str, Any]]:
|
|
935
|
+
"""Get all items mapped into (not owned by) the collection.
|
|
936
|
+
|
|
937
|
+
Parameters:
|
|
938
|
+
collection_uuid (str): UUID of the collection
|
|
939
|
+
all_languages (Optional[bool]): Whether to retrieve metadata in all languages
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
List[Dict[str, Any]]: List of mapped item metadata
|
|
943
|
+
"""
|
|
944
|
+
return list(self.iter_mapped_items(collection_uuid, all_languages))
|
|
945
|
+
|
|
946
|
+
def map_item(self, collection_uuid: str, item_uuid: str) -> bool:
|
|
947
|
+
"""Show an item in this collection too (no-op if already mapped; 422 if it is the owning collection).
|
|
948
|
+
|
|
949
|
+
Parameters:
|
|
950
|
+
collection_uuid (str): UUID of the collection
|
|
951
|
+
item_uuid (str): UUID of the item to map
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
bool: True if the item was mapped successfully
|
|
955
|
+
"""
|
|
956
|
+
self._request("POST", self._url(Endpoints.ITEMS, f"/{item_uuid}/mappedCollections"),
|
|
957
|
+
f"Map item {item_uuid} into {collection_uuid}", expected=(200, 201, 204),
|
|
958
|
+
headers=self._headers("text/uri-list"),
|
|
959
|
+
data=self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}"))
|
|
960
|
+
return True
|
|
961
|
+
|
|
962
|
+
def unmap_item(self, collection_uuid: str, item_uuid: str) -> bool:
|
|
963
|
+
"""Unmap an item from this collection.
|
|
964
|
+
|
|
965
|
+
Parameters:
|
|
966
|
+
collection_uuid (str): UUID of the collection
|
|
967
|
+
item_uuid (str): UUID of the item to unmap
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
bool: True if the item was unmapped successfully
|
|
971
|
+
"""
|
|
972
|
+
self._request("DELETE", self._url(Endpoints.ITEMS, f"/{item_uuid}/mappedCollections/{collection_uuid}"),
|
|
973
|
+
f"Unmap item {item_uuid} from {collection_uuid}", expected=(200, 204))
|
|
974
|
+
return True
|
|
975
|
+
|
|
976
|
+
def map_items(self, collection_uuid: str, item_uuids: Iterable[str]) -> Dict[str, str]:
|
|
977
|
+
"""Map several items; {item uuid: "mapped" | error message} - one failure does not stop the rest.
|
|
978
|
+
|
|
979
|
+
Parameters:
|
|
980
|
+
collection_uuid (str): UUID of the collection
|
|
981
|
+
item_uuids (Iterable[str]): UUIDs of the items to map
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
Dict[str, str]: Mapping of item UUIDs to "mapped" or error message
|
|
985
|
+
"""
|
|
986
|
+
from .exceptions import DSpaceError
|
|
987
|
+
result = {}
|
|
988
|
+
for item_uuid in item_uuids:
|
|
989
|
+
try:
|
|
990
|
+
self.map_item(collection_uuid, item_uuid)
|
|
991
|
+
result[item_uuid] = "mapped"
|
|
992
|
+
except DSpaceError as exc:
|
|
993
|
+
result[item_uuid] = f"error: {exc}"
|
|
994
|
+
return result
|
|
995
|
+
|
|
996
|
+
# ------------------------------------------------------------------
|
|
997
|
+
# OAI-PMH harvesting
|
|
998
|
+
# ------------------------------------------------------------------
|
|
999
|
+
|
|
1000
|
+
# Harvest types for OAI-PMH harvesting
|
|
1001
|
+
HARVEST_TYPES = ("NONE", "METADATA_ONLY", "METADATA_AND_REF", "METADATA_AND_BITSTREAMS")
|
|
1002
|
+
|
|
1003
|
+
def get_harvester_metadata_configs(self) -> List[Dict[str, Any]]:
|
|
1004
|
+
"""Get harvester metadata configurations.
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
List[Dict[str, Any]]: List of harvester metadata configurations, e.g.,
|
|
1008
|
+
[{"id": "dc", "label": "Simple Dublin Core", "nameSpace": ...}, ...]
|
|
1009
|
+
"""
|
|
1010
|
+
return self._get_json(self._url("/config/harvestermetadata"), "Get harvester metadata configs").get(
|
|
1011
|
+
"configs") or []
|
|
1012
|
+
|
|
1013
|
+
def get_harvester(self, collection_uuid: str) -> Dict[str, Any]:
|
|
1014
|
+
"""Harvest settings and status (collection admins only).
|
|
1015
|
+
|
|
1016
|
+
Parameters:
|
|
1017
|
+
collection_uuid (str): UUID of the collection
|
|
1018
|
+
|
|
1019
|
+
Returns:
|
|
1020
|
+
Dict[str, Any]: Harvester settings and status
|
|
1021
|
+
"""
|
|
1022
|
+
return self._get_json(self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}/harvester"),
|
|
1023
|
+
f"Get harvester settings of {collection_uuid}")
|
|
1024
|
+
|
|
1025
|
+
def set_harvester(self, collection_uuid: str, oai_source: str, oai_set_id: Optional[str] = None,
|
|
1026
|
+
metadata_config_id: str = "dc", harvest_type: str = "METADATA_ONLY") -> Dict[str, Any]:
|
|
1027
|
+
"""Turn the collection into an OAI-PMH harvested collection (the server checks the source).
|
|
1028
|
+
|
|
1029
|
+
Parameters:
|
|
1030
|
+
collection_uuid (str): UUID of the collection
|
|
1031
|
+
oai_source (str): OAI-PMH source URL
|
|
1032
|
+
oai_set_id (Optional[str]): OAI-PMH set ID
|
|
1033
|
+
metadata_config_id (str): Metadata configuration ID
|
|
1034
|
+
harvest_type (str): Harvest type, one of HARVEST_TYPES
|
|
1035
|
+
|
|
1036
|
+
Returns:
|
|
1037
|
+
Dict[str, Any]: Updated harvester settings
|
|
1038
|
+
"""
|
|
1039
|
+
if harvest_type not in self.HARVEST_TYPES:
|
|
1040
|
+
raise ValueError(f"harvest_type must be one of {self.HARVEST_TYPES}")
|
|
1041
|
+
body = {"harvest_type": harvest_type, "oai_source": oai_source, "oai_set_id": oai_set_id,
|
|
1042
|
+
"metadata_config_id": metadata_config_id}
|
|
1043
|
+
return self._put_harvester(collection_uuid, body)
|
|
1044
|
+
|
|
1045
|
+
def disable_harvesting(self, collection_uuid: str) -> Dict[str, Any]:
|
|
1046
|
+
"""Disable OAI-PMH harvesting for the collection.
|
|
1047
|
+
|
|
1048
|
+
Parameters:
|
|
1049
|
+
collection_uuid (str): UUID of the collection
|
|
1050
|
+
|
|
1051
|
+
Returns:
|
|
1052
|
+
Dict[str, Any]: Updated harvester settings
|
|
1053
|
+
"""
|
|
1054
|
+
return self._put_harvester(collection_uuid, {"harvest_type": "NONE"})
|
|
1055
|
+
|
|
1056
|
+
def _put_harvester(self, collection_uuid: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
1057
|
+
"""Update harvester settings for the collection.
|
|
1058
|
+
|
|
1059
|
+
Parameters:
|
|
1060
|
+
collection_uuid (str): UUID of the collection
|
|
1061
|
+
body (Dict[str, Any]): Request body containing harvester settings
|
|
1062
|
+
|
|
1063
|
+
Returns:
|
|
1064
|
+
Dict[str, Any]: Updated harvester settings
|
|
1065
|
+
"""
|
|
1066
|
+
response = self._request("PUT", self._url(Endpoints.COLLECTIONS, f"/{collection_uuid}/harvester"),
|
|
1067
|
+
f"Update harvester settings of {collection_uuid}", expected=(200, 204),
|
|
1068
|
+
headers=self._headers(), json=body)
|
|
1069
|
+
return self._json_or_none(response) or self.get_harvester(collection_uuid)
|