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/items.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Item management for DSpace API - handles archived items
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import json
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
from .config import Endpoints
|
|
10
|
+
from .projection import view_params
|
|
11
|
+
from .exceptions import handle_response_error, BadRequestError
|
|
12
|
+
|
|
13
|
+
# Logger for the module
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ItemManager:
|
|
18
|
+
"""Manages DSpace archived items"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, auth):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the ItemManager with authentication and configuration.
|
|
23
|
+
|
|
24
|
+
Parameters:
|
|
25
|
+
auth: Authentication object containing session and configuration.
|
|
26
|
+
"""
|
|
27
|
+
self.auth = auth
|
|
28
|
+
self.session = auth.session
|
|
29
|
+
self.config = auth.config
|
|
30
|
+
|
|
31
|
+
def get_items(self, page=0, size=20, sort=None, all_languages=None, projection=None, embed=None):
|
|
32
|
+
"""
|
|
33
|
+
Get a list of items from DSpace
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
page: Page number (0-based)
|
|
37
|
+
size: Number of items per page
|
|
38
|
+
sort: Sort criteria
|
|
39
|
+
all_languages: projection=allLanguages for this call (None = client setting)
|
|
40
|
+
projection / embed: shape the response, e.g. embed="owningCollection" (see projection.py)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
dict: API response containing items
|
|
44
|
+
"""
|
|
45
|
+
logger.info("Getting items (page=%d, size=%d)", page, size)
|
|
46
|
+
|
|
47
|
+
url = self.config.dspace_url + Endpoints.ITEMS
|
|
48
|
+
params = {"page": page, "size": size}
|
|
49
|
+
if sort:
|
|
50
|
+
params["sort"] = sort
|
|
51
|
+
params = view_params(params, self.config, all_languages, projection, embed)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
response = self.session.get(
|
|
55
|
+
url,
|
|
56
|
+
headers=self.config.headers,
|
|
57
|
+
params=params,
|
|
58
|
+
timeout=self.config.timeout
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if response.status_code == 200:
|
|
62
|
+
logger.info("Successfully retrieved items")
|
|
63
|
+
return response.json()
|
|
64
|
+
else:
|
|
65
|
+
handle_response_error(response, "Get items")
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error("Failed to get items: %s", e)
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
def get_item(self, item_uuid, all_languages=None, projection=None, embed=None):
|
|
72
|
+
"""
|
|
73
|
+
Get a specific item by UUID
|
|
74
|
+
|
|
75
|
+
Parameters:
|
|
76
|
+
item_uuid: UUID of the item
|
|
77
|
+
all_languages: projection=allLanguages for this call (None = client setting)
|
|
78
|
+
projection / embed: shape the response, e.g. embed=["bundles/bitstreams", "owningCollection"]
|
|
79
|
+
(see projection.py; read results with projection.get_embedded)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
dict: Item data
|
|
83
|
+
"""
|
|
84
|
+
logger.info("Getting item %s", item_uuid)
|
|
85
|
+
|
|
86
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}"
|
|
87
|
+
params = view_params(None, self.config, all_languages, projection, embed)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
response = self.session.get(
|
|
91
|
+
url,
|
|
92
|
+
headers=self.config.headers,
|
|
93
|
+
params=params,
|
|
94
|
+
timeout=self.config.timeout
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if response.status_code == 200:
|
|
98
|
+
logger.info("Successfully retrieved item %s", item_uuid)
|
|
99
|
+
return response.json()
|
|
100
|
+
else:
|
|
101
|
+
handle_response_error(response, f"Get item {item_uuid}")
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error("Failed to get item %s: %s", item_uuid, e)
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
def create_item(self, collection_uuid, metadata, entity_type,
|
|
108
|
+
in_archive=True, discoverable=True, withdrawn=False):
|
|
109
|
+
"""
|
|
110
|
+
Create an archived item directly in a collection
|
|
111
|
+
|
|
112
|
+
Parameters:
|
|
113
|
+
collection_uuid: UUID of the owning collection
|
|
114
|
+
metadata: Dictionary of metadata fields
|
|
115
|
+
entity_type: Type of entity (e.g., "Publication"); None for a plain item
|
|
116
|
+
in_archive: Whether item should be archived immediately
|
|
117
|
+
discoverable: Whether item should be discoverable
|
|
118
|
+
withdrawn: Whether item is withdrawn
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
dict: Created item data
|
|
122
|
+
"""
|
|
123
|
+
logger.info("Creating item in collection %s", collection_uuid)
|
|
124
|
+
|
|
125
|
+
# Prepare item data
|
|
126
|
+
item_data = {
|
|
127
|
+
"metadata": self._format_metadata(metadata),
|
|
128
|
+
"inArchive": in_archive,
|
|
129
|
+
"discoverable": discoverable,
|
|
130
|
+
"withdrawn": withdrawn,
|
|
131
|
+
"type": "item"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# entity_type=None: plain item (repositories without configurable entities)
|
|
135
|
+
if entity_type:
|
|
136
|
+
item_data["entityType"] = entity_type
|
|
137
|
+
# Add entity type to metadata if not present
|
|
138
|
+
if "dspace.entity.type" not in item_data["metadata"]:
|
|
139
|
+
item_data["metadata"]["dspace.entity.type"] = [{"value": entity_type}]
|
|
140
|
+
|
|
141
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}?owningCollection={collection_uuid}"
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
response = self.session.post(
|
|
145
|
+
url,
|
|
146
|
+
headers=self.config.headers,
|
|
147
|
+
data=json.dumps(item_data),
|
|
148
|
+
timeout=self.config.timeout
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if response.status_code == 201:
|
|
152
|
+
logger.info("Item created successfully")
|
|
153
|
+
return response.json()
|
|
154
|
+
else:
|
|
155
|
+
handle_response_error(response, "Create item")
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error("Failed to create item: %s", e)
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
def update_item(self, item_uuid, operation, metadata):
|
|
162
|
+
"""
|
|
163
|
+
Update item metadata
|
|
164
|
+
|
|
165
|
+
Parameters:
|
|
166
|
+
item_uuid: UUID of the item to update
|
|
167
|
+
operation: JSON-Patch operation to perform (e.g., "add", "replace", "remove")
|
|
168
|
+
metadata: Dictionary of metadata fields to update
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
dict: Updated item data
|
|
172
|
+
"""
|
|
173
|
+
logger.info("Updating item %s", item_uuid)
|
|
174
|
+
|
|
175
|
+
# Create patch operations for metadata updates
|
|
176
|
+
patch_operations = []
|
|
177
|
+
|
|
178
|
+
for field, values in metadata.items():
|
|
179
|
+
patch_operations.append({
|
|
180
|
+
"op": operation, # add, replace, remove
|
|
181
|
+
"path": f"/metadata/{field}",
|
|
182
|
+
"value": self._format_metadata_field(values)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}"
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
response = self.session.patch(
|
|
189
|
+
url,
|
|
190
|
+
headers=self.config.headers,
|
|
191
|
+
json=patch_operations,
|
|
192
|
+
timeout=self.config.timeout
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if response.status_code == 200:
|
|
196
|
+
logger.info("Item %s updated successfully", item_uuid)
|
|
197
|
+
return response.json()
|
|
198
|
+
else:
|
|
199
|
+
handle_response_error(response, f"Update item {item_uuid}")
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error("Failed to update item %s: %s", item_uuid, e)
|
|
203
|
+
raise
|
|
204
|
+
|
|
205
|
+
def upsert_metadata(self, item_uuid, metadata, existing_metadata=None):
|
|
206
|
+
"""
|
|
207
|
+
Add-or-replace metadata fields on an already-archived item, choosing
|
|
208
|
+
the correct JSON-Patch op per field automatically.
|
|
209
|
+
|
|
210
|
+
Plain PATCH "replace" on a metadata path that doesn't exist yet on
|
|
211
|
+
the item can be rejected by DSpace, and plain "add" on a path that
|
|
212
|
+
already has values just appends (creating duplicates) instead of
|
|
213
|
+
overwriting. This method looks at what the item currently has and
|
|
214
|
+
picks "replace" for fields it already carries and "add" for fields
|
|
215
|
+
that are genuinely new, so a single call is safe to use both for
|
|
216
|
+
first-time enrichment and for repeat syncs.
|
|
217
|
+
|
|
218
|
+
Parameters:
|
|
219
|
+
item_uuid: UUID of the item to update
|
|
220
|
+
metadata: dict of field -> value(s). Values may be plain
|
|
221
|
+
strings, lists of strings, or lists of pre-formatted
|
|
222
|
+
metadata-value dicts (e.g. to set authority/confidence for
|
|
223
|
+
a controlled or linked field such as dc.relation.project).
|
|
224
|
+
existing_metadata: optional dict as returned by item["metadata"]
|
|
225
|
+
(field -> list of value dicts). If omitted, the item is
|
|
226
|
+
fetched first so the add/replace decision can be made
|
|
227
|
+
safely.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
dict: updated item data
|
|
231
|
+
"""
|
|
232
|
+
if existing_metadata is None:
|
|
233
|
+
# all languages: a field with values only in another language still exists
|
|
234
|
+
current = self.get_item(item_uuid, all_languages=True)
|
|
235
|
+
existing_metadata = current.get("metadata", {}) or {}
|
|
236
|
+
|
|
237
|
+
logger.info("Upserting metadata on item %s (%d field(s))", item_uuid, len(metadata))
|
|
238
|
+
|
|
239
|
+
patch_operations = []
|
|
240
|
+
for field, values in metadata.items():
|
|
241
|
+
op = "replace" if existing_metadata.get(field) else "add"
|
|
242
|
+
patch_operations.append({
|
|
243
|
+
"op": op,
|
|
244
|
+
"path": f"/metadata/{field}",
|
|
245
|
+
"value": self._format_metadata_field(values),
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}"
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
response = self.session.patch(
|
|
252
|
+
url,
|
|
253
|
+
headers=self.config.headers,
|
|
254
|
+
json=patch_operations,
|
|
255
|
+
timeout=self.config.timeout
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if response.status_code == 200:
|
|
259
|
+
logger.info("Item %s upserted successfully", item_uuid)
|
|
260
|
+
return response.json()
|
|
261
|
+
else:
|
|
262
|
+
handle_response_error(response, f"Upsert item {item_uuid}")
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error("Failed to upsert item %s: %s", item_uuid, e)
|
|
266
|
+
raise
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# STT -------------------------------------------------------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
def upsert_metadata_by_position(self, item_uuid, metadata):
|
|
272
|
+
"""
|
|
273
|
+
IMPORTANT: This function is an adapted version of 'upsert_metadata' because as of 2026-07-29 there is an issue with patch when the field is a list.
|
|
274
|
+
https://github.com/DSpace/DSpace/issues/12419
|
|
275
|
+
https://github.com/DSpace/DSpace/pull/12428
|
|
276
|
+
https://github.com/DSpace/DSpace/pull/12467
|
|
277
|
+
|
|
278
|
+
Add-or-replace metadata fields on an already-archived item, choosing
|
|
279
|
+
the correct JSON-Patch op per field automatically depending on metadata place-value.
|
|
280
|
+
|
|
281
|
+
Plain PATCH "replace" on a metadata path that doesn't exist yet on
|
|
282
|
+
the item can be rejected by DSpace, and plain "add" on a path that
|
|
283
|
+
already has values just appends (creating duplicates) instead of
|
|
284
|
+
overwriting. This method looks at what the item currently has and
|
|
285
|
+
picks "replace" for fields it already carries and "add" for fields
|
|
286
|
+
that are genuinely new, so a single call is safe to use both for
|
|
287
|
+
first-time enrichment and for repeat syncs.
|
|
288
|
+
|
|
289
|
+
Parameters:
|
|
290
|
+
item_uuid: UUID of the item to update
|
|
291
|
+
metadata: dict of field -> value(s). Values has to be a pre-formatted
|
|
292
|
+
metadata-value dict (e.g. to set authority/confidence for
|
|
293
|
+
a controlled or linked field such as dc.relation.project).
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
dict: updated item data
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
# the item is fetched first so the add/replace decision can be made safely
|
|
300
|
+
# existing_metadata is a dict as returned by item["metadata"] (field -> list of value dicts)
|
|
301
|
+
# all languages: PATCH positions refer to the complete value list, not the
|
|
302
|
+
# locale-filtered one, otherwise "place" would point at the wrong value
|
|
303
|
+
current = self.get_item(item_uuid, all_languages=True)
|
|
304
|
+
existing_metadata = current.get("metadata", {}) or {}
|
|
305
|
+
|
|
306
|
+
logger.info("Upserting metadata on item %s (%d field(s))", item_uuid, len(metadata))
|
|
307
|
+
|
|
308
|
+
patch_operations = []
|
|
309
|
+
for field, values in metadata.items():
|
|
310
|
+
metadata_position = values.get("place")
|
|
311
|
+
|
|
312
|
+
if not isinstance(metadata_position, int):
|
|
313
|
+
raise TypeError(f"Item={item_uuid}: place-value has to be a valid integer. place: {metadata_position}")
|
|
314
|
+
|
|
315
|
+
if existing_metadata.get(field):
|
|
316
|
+
# check place-value of metadata to see if we have an 'replace' or 'add' operation
|
|
317
|
+
|
|
318
|
+
# get number of entries in existing metadata field
|
|
319
|
+
num_entries_existing_metadata_field = len(existing_metadata.get(field))
|
|
320
|
+
|
|
321
|
+
if metadata_position < num_entries_existing_metadata_field:
|
|
322
|
+
# replace at metadata_position
|
|
323
|
+
|
|
324
|
+
patch_operations.append({
|
|
325
|
+
"op": "replace",
|
|
326
|
+
"path": f"/metadata/{field}/{metadata_position}",
|
|
327
|
+
"value": values if isinstance(values,dict) else self._format_metadata_field(values),
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
elif metadata_position == num_entries_existing_metadata_field:
|
|
331
|
+
# add at the end without changing existing data
|
|
332
|
+
patch_operations.append({
|
|
333
|
+
"op": "add",
|
|
334
|
+
"path": f"/metadata/{field}/-",
|
|
335
|
+
"value": values if isinstance(values,dict) else self._format_metadata_field(values),
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
else:
|
|
339
|
+
raise IndexError(f"Item={item_uuid}: The place-value of metadata is too big to be added as new metadata. metadata_place={metadata_position} next_valid_place={num_entries_existing_metadata_field}")
|
|
340
|
+
elif metadata_position == 0:
|
|
341
|
+
# add new metadata field
|
|
342
|
+
patch_operations.append({
|
|
343
|
+
"op": "add",
|
|
344
|
+
"path": f"/metadata/{field}",
|
|
345
|
+
"value": values if isinstance(values,dict) else self._format_metadata_field(values),
|
|
346
|
+
})
|
|
347
|
+
else:
|
|
348
|
+
raise IndexError(f"Item={item_uuid}: The place-value of metadata has to be 0 as it would be the first entry for this new metadata field. metadata_place={metadata_position}")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}"
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
response = self.session.patch(
|
|
355
|
+
url,
|
|
356
|
+
headers=self.config.headers,
|
|
357
|
+
json=patch_operations,
|
|
358
|
+
timeout=self.config.timeout
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if response.status_code == 200:
|
|
362
|
+
logger.info("Item %s upserted successfully", item_uuid)
|
|
363
|
+
return response.json()
|
|
364
|
+
else:
|
|
365
|
+
handle_response_error(response, f"Upsert item {item_uuid}")
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error("Failed to upsert item %s: %s", item_uuid, e)
|
|
369
|
+
raise
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def delete_item(self, item_uuid):
|
|
373
|
+
"""
|
|
374
|
+
Delete an item
|
|
375
|
+
|
|
376
|
+
Parameters:
|
|
377
|
+
item_uuid: UUID of the item to delete
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
bool: True if deletion was successful
|
|
381
|
+
"""
|
|
382
|
+
logger.info("Deleting item %s", item_uuid)
|
|
383
|
+
|
|
384
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}"
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
response = self.session.delete(
|
|
388
|
+
url,
|
|
389
|
+
headers=self.config.headers,
|
|
390
|
+
timeout=self.config.timeout
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if response.status_code == 204:
|
|
394
|
+
logger.info("Item %s deleted successfully", item_uuid)
|
|
395
|
+
return True
|
|
396
|
+
else:
|
|
397
|
+
handle_response_error(response, f"Delete item {item_uuid}")
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error("Failed to delete item %s: %s", item_uuid, e)
|
|
401
|
+
raise
|
|
402
|
+
|
|
403
|
+
def change_owning_collection(self, item_uuid, new_collection_uuid, inherit_policies=False):
|
|
404
|
+
"""
|
|
405
|
+
Change the owning collection of an item
|
|
406
|
+
|
|
407
|
+
Parameters:
|
|
408
|
+
item_uuid: UUID of the item
|
|
409
|
+
new_collection_uuid: UUID of the new owning collection
|
|
410
|
+
inherit_policies: Whether to inherit policies from new collection
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
bool: True if successful
|
|
414
|
+
"""
|
|
415
|
+
logger.info("Changing owning collection of item %s to %s", item_uuid, new_collection_uuid)
|
|
416
|
+
|
|
417
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}/owningCollection"
|
|
418
|
+
if inherit_policies:
|
|
419
|
+
url += "?inheritPolicies=true"
|
|
420
|
+
|
|
421
|
+
headers = {"Content-Type": "text/uri-list"}
|
|
422
|
+
data = f"{self.config.dspace_url}/core/collections/{new_collection_uuid}"
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
response = self.session.put(
|
|
426
|
+
url,
|
|
427
|
+
headers=headers,
|
|
428
|
+
data=data,
|
|
429
|
+
timeout=self.config.timeout
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if response.status_code in (200, 204):
|
|
433
|
+
logger.info("Owning collection updated successfully")
|
|
434
|
+
return True
|
|
435
|
+
else:
|
|
436
|
+
handle_response_error(response, "Change owning collection")
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error("Failed to change owning collection: %s", e)
|
|
440
|
+
raise
|
|
441
|
+
|
|
442
|
+
def add_mapped_collection(self, item_uuid, collection_uuid):
|
|
443
|
+
"""
|
|
444
|
+
Add a mapped collection to an item
|
|
445
|
+
|
|
446
|
+
Parameters:
|
|
447
|
+
item_uuid: UUID of the item
|
|
448
|
+
collection_uuid: UUID of the collection to map
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
bool: True if successful
|
|
452
|
+
"""
|
|
453
|
+
logger.info("Mapping item %s to collection %s", item_uuid, collection_uuid)
|
|
454
|
+
|
|
455
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}/mappedCollections"
|
|
456
|
+
headers = {"Content-Type": "text/uri-list"}
|
|
457
|
+
data = f"{self.config.dspace_url}/core/collections/{collection_uuid}"
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
response = self.session.post(
|
|
461
|
+
url,
|
|
462
|
+
headers=headers,
|
|
463
|
+
data=data,
|
|
464
|
+
timeout=self.config.timeout
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if response.status_code == 204:
|
|
468
|
+
logger.info("Mapped collection added successfully")
|
|
469
|
+
return True
|
|
470
|
+
else:
|
|
471
|
+
handle_response_error(response, "Add mapped collection")
|
|
472
|
+
|
|
473
|
+
except Exception as e:
|
|
474
|
+
logger.error("Failed to add mapped collection: %s", e)
|
|
475
|
+
raise
|
|
476
|
+
|
|
477
|
+
def remove_mapped_collection(self, item_uuid, collection_uuid):
|
|
478
|
+
"""
|
|
479
|
+
Remove a mapped collection from an item
|
|
480
|
+
|
|
481
|
+
Parameters:
|
|
482
|
+
item_uuid: UUID of the item
|
|
483
|
+
collection_uuid: UUID of the collection to unmap
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
bool: True if successful
|
|
487
|
+
"""
|
|
488
|
+
logger.info("Removing mapped collection %s from item %s", collection_uuid, item_uuid)
|
|
489
|
+
|
|
490
|
+
url = f"{self.config.dspace_url}{Endpoints.ITEMS}/{item_uuid}/mappedCollections/{collection_uuid}"
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
response = self.session.delete(
|
|
494
|
+
url,
|
|
495
|
+
headers=self.config.headers,
|
|
496
|
+
timeout=self.config.timeout
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if response.status_code == 204:
|
|
500
|
+
logger.info("Mapped collection removed successfully")
|
|
501
|
+
return True
|
|
502
|
+
else:
|
|
503
|
+
handle_response_error(response, "Remove mapped collection")
|
|
504
|
+
|
|
505
|
+
except Exception as e:
|
|
506
|
+
logger.error("Failed to remove mapped collection: %s", e)
|
|
507
|
+
raise
|
|
508
|
+
|
|
509
|
+
def _format_metadata(self, metadata_dict):
|
|
510
|
+
"""
|
|
511
|
+
Format metadata dictionary for DSpace API
|
|
512
|
+
|
|
513
|
+
Parameters:
|
|
514
|
+
metadata_dict: Dictionary of metadata fields
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
dict: Formatted metadata
|
|
518
|
+
"""
|
|
519
|
+
formatted = {}
|
|
520
|
+
|
|
521
|
+
for field, values in metadata_dict.items():
|
|
522
|
+
formatted[field] = self._format_metadata_field(values)
|
|
523
|
+
|
|
524
|
+
return formatted
|
|
525
|
+
|
|
526
|
+
def _format_metadata_field(self, values):
|
|
527
|
+
"""
|
|
528
|
+
Format metadata field values for DSpace API
|
|
529
|
+
|
|
530
|
+
Parameters:
|
|
531
|
+
values: String, list of strings, or list of dicts
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
list: Formatted metadata values
|
|
535
|
+
"""
|
|
536
|
+
if isinstance(values, str):
|
|
537
|
+
return [{"value": values}]
|
|
538
|
+
elif isinstance(values, list):
|
|
539
|
+
formatted = []
|
|
540
|
+
for value in values:
|
|
541
|
+
if isinstance(value, str):
|
|
542
|
+
formatted.append({"value": value})
|
|
543
|
+
elif isinstance(value, dict):
|
|
544
|
+
formatted.append(value)
|
|
545
|
+
else:
|
|
546
|
+
formatted.append({"value": str(value)})
|
|
547
|
+
return formatted
|
|
548
|
+
else:
|
|
549
|
+
return [{"value": str(values)}]
|