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/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""DSpace API toolkit"""
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .config import DSpaceConfig
|
|
5
|
+
from .auth import DSpaceAuth
|
|
6
|
+
from .items import ItemManager
|
|
7
|
+
from .workspaces import WorkspaceManager
|
|
8
|
+
from .bundles import BundleManager
|
|
9
|
+
from .collections import CollectionManager
|
|
10
|
+
from .discovery import DiscoveryManager
|
|
11
|
+
from .vocabulary import VocabularyManager
|
|
12
|
+
from .permissions import PermissionManager
|
|
13
|
+
from .communities import CommunityManager
|
|
14
|
+
from .groups import GroupManager
|
|
15
|
+
from .epersons import EPersonManager
|
|
16
|
+
from .site import SiteManager
|
|
17
|
+
from .entity_types import EntityTypeManager
|
|
18
|
+
from .metadata import MetadataManager, MetadataPatch
|
|
19
|
+
from .authorizations import AuthorizationManager, Feature
|
|
20
|
+
from .versions import VersionManager
|
|
21
|
+
from .relationships import RelationshipManager
|
|
22
|
+
from .workflow import WorkflowError, WorkflowManager
|
|
23
|
+
from .workspaces import SubmissionError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DSpaceClient:
|
|
27
|
+
"""Client for interacting with the DSpace REST API."""
|
|
28
|
+
def __init__(self, dspace_url=None, username=None, password=None, version=8, all_languages=None):
|
|
29
|
+
"""Initialize the DSpace client with optional URL, credentials, API version, and language settings.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
dspace_url: The base URL of the DSpace instance.
|
|
33
|
+
username: The username for authentication.
|
|
34
|
+
password: The password for authentication.
|
|
35
|
+
version: The API version to use (default is 8).
|
|
36
|
+
all_languages: Optional flag for handling all languages in metadata.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
# all_languages: default for projection=allLanguages on reads
|
|
40
|
+
# (None -> DSPACE_ALL_LANGUAGES env var, default off)
|
|
41
|
+
self.config = DSpaceConfig(dspace_url, version, all_languages=all_languages)
|
|
42
|
+
# Initialize authentication manager with the given configuration.
|
|
43
|
+
self.auth = DSpaceAuth(self.config)
|
|
44
|
+
# Log in if username and password are provided.
|
|
45
|
+
if username and password:
|
|
46
|
+
self.auth.login(username, password)
|
|
47
|
+
# Initialize managers for various DSpace API components.
|
|
48
|
+
self.items = ItemManager(self.auth)
|
|
49
|
+
self.workspace = WorkspaceManager(self.auth)
|
|
50
|
+
self.bundles = BundleManager(self.auth)
|
|
51
|
+
self.collections = CollectionManager(self.auth)
|
|
52
|
+
self.discovery = DiscoveryManager(self.auth)
|
|
53
|
+
self.vocabulary = VocabularyManager(self.auth)
|
|
54
|
+
self.permissions = PermissionManager(self.auth)
|
|
55
|
+
self.communities = CommunityManager(self.auth)
|
|
56
|
+
self.groups = GroupManager(self.auth)
|
|
57
|
+
self.epersons = EPersonManager(self.auth)
|
|
58
|
+
self.site = SiteManager(self.auth)
|
|
59
|
+
self.entity_types = EntityTypeManager(self.auth)
|
|
60
|
+
self.metadata = MetadataManager(self.auth)
|
|
61
|
+
self.authorizations = AuthorizationManager(self.auth)
|
|
62
|
+
self.versions = VersionManager(self.auth)
|
|
63
|
+
self.relationships = RelationshipManager(self.auth)
|
|
64
|
+
self.workflow = WorkflowManager(self.auth)
|
|
65
|
+
|
|
66
|
+
def close(self):
|
|
67
|
+
"""Close the client and log out from the DSpace API.
|
|
68
|
+
|
|
69
|
+
Parameters:
|
|
70
|
+
None
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
None
|
|
74
|
+
"""
|
|
75
|
+
self.auth.logout()
|
dspace_api/_base.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Shared request handling for DSpace API managers.
|
|
4
|
+
|
|
5
|
+
Every manager that talks to the REST API through ``session.request()``
|
|
6
|
+
inherits from BaseManager, so they all get the same behaviour:
|
|
7
|
+
|
|
8
|
+
- the session is always the auth object's 'current' one
|
|
9
|
+
(DSpaceAuth.logout() replaces it)
|
|
10
|
+
- CSRF tokens returned by DSpace are stored after every response; retries,
|
|
11
|
+
token refresh, re-login and the CSRF retry live in DSpaceSession
|
|
12
|
+
- HAL collections are paged transparently
|
|
13
|
+
- links from ``_links.*.href`` can be followed, but only on the
|
|
14
|
+
configured DSpace server (the session carries the auth header)
|
|
15
|
+
- GET requests take ``projection=`` and ``embed=`` (see projection.py),
|
|
16
|
+
and get ``projection=allLanguages`` when enabled - per call
|
|
17
|
+
(``all_languages=True/False``), in a ``config.languages()`` block, or
|
|
18
|
+
as the config default (``DSpaceConfig(all_languages=...)`` /
|
|
19
|
+
``DSPACE_ALL_LANGUAGES``)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Any, Dict, Iterator, Optional, Sequence, Tuple
|
|
24
|
+
|
|
25
|
+
from .projection import EmbedArg, ProjectionArg, view_params
|
|
26
|
+
from .exceptions import handle_response_error
|
|
27
|
+
from .utils import extract_uuid_from_url, validate_uuid
|
|
28
|
+
|
|
29
|
+
# Logging setup for the module
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# Constants for DSpace API headers
|
|
33
|
+
|
|
34
|
+
# Header for the DSpace XSRF token returned in responses
|
|
35
|
+
XSRF_RESPONSE_HEADER = "DSPACE-XSRF-TOKEN"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseManager:
|
|
39
|
+
"""Base class with request, paging and link helpers"""
|
|
40
|
+
|
|
41
|
+
DEFAULT_PAGE_SIZE = 100
|
|
42
|
+
|
|
43
|
+
def __init__(self, auth):
|
|
44
|
+
"""Store the authentication manager and configuration.
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
auth: The authentication manager instance.
|
|
48
|
+
"""
|
|
49
|
+
self.auth = auth
|
|
50
|
+
self.config = auth.config
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def session(self):
|
|
54
|
+
"""Return the current session from the authentication manager.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The current session object.
|
|
58
|
+
"""
|
|
59
|
+
return self.auth.session
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
# URLs and headers
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def _url(self, *parts: str) -> str:
|
|
66
|
+
"""Construct a full URL by joining the base DSpace URL with the given path parts.
|
|
67
|
+
|
|
68
|
+
Parameters:
|
|
69
|
+
*parts: The path parts to join with the base DSpace URL.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The full URL as a string.
|
|
73
|
+
"""
|
|
74
|
+
return self.config.dspace_url + "".join(str(p) for p in parts)
|
|
75
|
+
|
|
76
|
+
# A uri-list body is just the absolute URL of the referenced resource
|
|
77
|
+
_uri_list = _url
|
|
78
|
+
|
|
79
|
+
def _headers(self, content_type: Optional[str] = "application/json", accept: str = "application/json"):
|
|
80
|
+
"""Construct the headers for a request, including content type and accept headers.
|
|
81
|
+
|
|
82
|
+
Parameters:
|
|
83
|
+
content_type: The value for the 'Content-Type' header, or None to omit it.
|
|
84
|
+
accept: The value for the 'Accept' header.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A dictionary of headers for the request.
|
|
88
|
+
"""
|
|
89
|
+
headers = {k: v for k, v in self.config.headers.items() if k.lower() not in ("content-type", "accept")}
|
|
90
|
+
headers["Accept"] = accept
|
|
91
|
+
if content_type:
|
|
92
|
+
headers["Content-Type"] = content_type
|
|
93
|
+
return headers
|
|
94
|
+
|
|
95
|
+
def resolve_href(self, href: str) -> str:
|
|
96
|
+
"""Turn a HAL href or an '/api/...' path into an absolute URL on the
|
|
97
|
+
configured server. Absolute URLs on other hosts are refused, because
|
|
98
|
+
the session would send the Authorization header along.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
href: The HAL href or '/api/...' path to resolve.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The absolute URL on the configured server.
|
|
105
|
+
"""
|
|
106
|
+
if href.startswith(("http://", "https://")):
|
|
107
|
+
if not href.startswith(self.config.dspace_url):
|
|
108
|
+
raise ValueError(f"Refusing to follow {href!r}: not under {self.config.dspace_url}")
|
|
109
|
+
return href
|
|
110
|
+
if href.startswith("/api/") and self.config.dspace_url.endswith("/api"):
|
|
111
|
+
return self.config.dspace_url[: -len("/api")] + href
|
|
112
|
+
return self._url("/" + href.lstrip("/"))
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Requests
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def _update_token(self, response) -> None:
|
|
119
|
+
"""Update the authentication token based on the response, if applicable."""
|
|
120
|
+
update = getattr(self.auth, "_update_token", None)
|
|
121
|
+
if update:
|
|
122
|
+
update(response)
|
|
123
|
+
|
|
124
|
+
def _request(self, method: str, url: str, operation: str, expected=(200,),
|
|
125
|
+
all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None, **kwargs):
|
|
126
|
+
"""Perform an HTTP request with the given method, URL, and parameters.
|
|
127
|
+
Handles projection, embedding, and language settings for GET requests.
|
|
128
|
+
|
|
129
|
+
Parameters:
|
|
130
|
+
method: The HTTP method to use (e.g., "GET", "POST").
|
|
131
|
+
url: The URL to send the request to.
|
|
132
|
+
operation: A description of the operation for logging or error messages.
|
|
133
|
+
expected: A tuple of expected HTTP status codes.
|
|
134
|
+
all_languages: Whether to retrieve all language variants (for GET requests).
|
|
135
|
+
projection: Optional HAL projection parameter (for GET requests).
|
|
136
|
+
embed: Optional HAL embed parameter (for GET requests).
|
|
137
|
+
**kwargs: Additional arguments to pass to the request.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The HTTP response object.
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
if method.upper() == "GET":
|
|
144
|
+
kwargs["params"] = view_params(kwargs.get("params"), self.config, all_languages, projection, embed)
|
|
145
|
+
elif projection or embed:
|
|
146
|
+
raise ValueError("projection/embed only apply to GET requests")
|
|
147
|
+
kwargs.setdefault("headers", self.config.headers)
|
|
148
|
+
kwargs.setdefault("timeout", self.config.timeout)
|
|
149
|
+
# Retries, token refresh, re-login and the CSRF retry happen in
|
|
150
|
+
# DSpaceSession (see session.py), so every manager gets them.
|
|
151
|
+
response = self.session.request(method, url, **kwargs)
|
|
152
|
+
self._update_token(response)
|
|
153
|
+
|
|
154
|
+
if response.status_code not in expected:
|
|
155
|
+
logger.error("%s failed: HTTP %s %s", operation, response.status_code, response.text[:500])
|
|
156
|
+
handle_response_error(response, operation)
|
|
157
|
+
return response
|
|
158
|
+
|
|
159
|
+
def _get_json(self, url: str, operation: str, params=None,
|
|
160
|
+
all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
161
|
+
"""Perform a GET request and return the JSON response as a dictionary.
|
|
162
|
+
|
|
163
|
+
Parameters:
|
|
164
|
+
url: The URL to send the GET request to.
|
|
165
|
+
operation: A description of the operation for logging or error messages.
|
|
166
|
+
params: Optional query parameters as a dictionary.
|
|
167
|
+
all_languages: Whether to retrieve all language variants.
|
|
168
|
+
projection: Optional HAL projection parameter.
|
|
169
|
+
embed: Optional HAL embed parameter.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The JSON response as a dictionary.
|
|
173
|
+
"""
|
|
174
|
+
return self._request("GET", url, operation, params=params, all_languages=all_languages,
|
|
175
|
+
projection=projection, embed=embed).json()
|
|
176
|
+
|
|
177
|
+
def _get_optional(self, url: str, operation: str, params=None,
|
|
178
|
+
all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Optional[Dict[str, Any]]:
|
|
179
|
+
"""Perform a GET request that maps '204 No Content' (and an empty 200) to None.
|
|
180
|
+
|
|
181
|
+
Parameters:
|
|
182
|
+
url: The URL to send the GET request to.
|
|
183
|
+
operation: A description of the operation for logging or error messages.
|
|
184
|
+
params: Optional query parameters as a dictionary.
|
|
185
|
+
all_languages: Whether to retrieve all language variants.
|
|
186
|
+
projection: Optional HAL projection parameter.
|
|
187
|
+
embed: Optional HAL embed parameter.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
The JSON response as a dictionary, or None if there is no content.
|
|
191
|
+
"""
|
|
192
|
+
response = self._request("GET", url, operation, expected=(200, 204), params=params,
|
|
193
|
+
all_languages=all_languages, projection=projection, embed=embed)
|
|
194
|
+
if response.status_code == 204 or not response.content:
|
|
195
|
+
return None
|
|
196
|
+
return response.json() or None
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _json_or_none(response) -> Optional[Dict[str, Any]]:
|
|
200
|
+
"""Return the JSON content of the response, or None if there is no content.
|
|
201
|
+
|
|
202
|
+
Parameters:
|
|
203
|
+
response: The HTTP response object.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The JSON content as a dictionary, or None if there is no content.
|
|
207
|
+
"""
|
|
208
|
+
return response.json() if response.content else None
|
|
209
|
+
|
|
210
|
+
def _iter_embedded(self, url: str, key, operation: str,
|
|
211
|
+
params: Optional[Sequence[Tuple[str, Any]]] = None,
|
|
212
|
+
page_size: Optional[int] = None,
|
|
213
|
+
max_results: Optional[int] = None,
|
|
214
|
+
all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Iterator[Dict[str, Any]]:
|
|
215
|
+
"""
|
|
216
|
+
Yield every object of a paginated HAL collection (_embedded.<key>).
|
|
217
|
+
key may be a tuple of alternative names (first one present wins).
|
|
218
|
+
params is a list of tuples so repeated keys (embed=a&embed=b) work.
|
|
219
|
+
|
|
220
|
+
Parameters:
|
|
221
|
+
url: The URL of the HAL collection.
|
|
222
|
+
key: The key or tuple of alternative keys to look for in the _embedded section.
|
|
223
|
+
operation: A description of the operation for logging or error messages.
|
|
224
|
+
params: Optional query parameters as a list of key-value pairs.
|
|
225
|
+
page_size: The number of results per page.
|
|
226
|
+
max_results: The maximum number of results to yield.
|
|
227
|
+
all_languages: Whether to retrieve all language variants.
|
|
228
|
+
projection: Optional HAL projection parameter.
|
|
229
|
+
embed: Optional HAL embed parameter.
|
|
230
|
+
|
|
231
|
+
Yields:
|
|
232
|
+
Each object in the paginated HAL collection.
|
|
233
|
+
"""
|
|
234
|
+
keys = (key,) if isinstance(key, str) else tuple(key)
|
|
235
|
+
size = page_size or self.DEFAULT_PAGE_SIZE
|
|
236
|
+
page = 0
|
|
237
|
+
yielded = 0
|
|
238
|
+
while True:
|
|
239
|
+
p = list(params or []) + [("page", page), ("size", size)]
|
|
240
|
+
data = self._request("GET", url, operation, params=p, all_languages=all_languages,
|
|
241
|
+
projection=projection, embed=embed).json()
|
|
242
|
+
embedded = data.get("_embedded", {})
|
|
243
|
+
objects = next((embedded[k] for k in keys if k in embedded), [])
|
|
244
|
+
for obj in objects:
|
|
245
|
+
yield obj
|
|
246
|
+
yielded += 1
|
|
247
|
+
if max_results is not None and yielded >= max_results:
|
|
248
|
+
return
|
|
249
|
+
total_pages = (data.get("page") or {}).get("totalPages")
|
|
250
|
+
if not objects:
|
|
251
|
+
break
|
|
252
|
+
if total_pages is not None and page + 1 >= total_pages:
|
|
253
|
+
break
|
|
254
|
+
if total_pages is None and len(objects) < size:
|
|
255
|
+
break
|
|
256
|
+
page += 1
|
|
257
|
+
|
|
258
|
+
def _discover(self, query: str = "", dso_type: Optional[str] = None, configuration: Optional[str] = None,
|
|
259
|
+
scope: Optional[str] = None, sort: Optional[str] = None, size: int = 20,
|
|
260
|
+
max_results: Optional[int] = None, all_languages: Optional[bool] = None,
|
|
261
|
+
projection: ProjectionArg = None, embed: EmbedArg = None):
|
|
262
|
+
"""Perform a discovery search returning the indexableObjects (all pages up to max_results).
|
|
263
|
+
|
|
264
|
+
Parameters:
|
|
265
|
+
query: The search query string.
|
|
266
|
+
dso_type: The type of DSpace object to search for.
|
|
267
|
+
configuration: The discovery configuration to use.
|
|
268
|
+
scope: The scope of the search.
|
|
269
|
+
sort: The sort order of the results.
|
|
270
|
+
size: The number of results per page.
|
|
271
|
+
max_results: The maximum number of results to return.
|
|
272
|
+
all_languages: Whether to retrieve all language variants.
|
|
273
|
+
projection: Optional HAL projection parameter.
|
|
274
|
+
embed: Optional HAL embed parameter.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
A list of indexable DSpace objects matching the search criteria.
|
|
278
|
+
"""
|
|
279
|
+
url = self._url("/discover/search/objects")
|
|
280
|
+
page = 0
|
|
281
|
+
found = []
|
|
282
|
+
while True:
|
|
283
|
+
params = [("page", page), ("size", size)]
|
|
284
|
+
for key, value in (("query", query.strip() if query else None), ("dsoType", dso_type),
|
|
285
|
+
("configuration", configuration), ("scope", scope), ("sort", sort)):
|
|
286
|
+
if value:
|
|
287
|
+
params.append((key, value))
|
|
288
|
+
result = (self._get_json(url, "Discovery search", params, all_languages=all_languages,
|
|
289
|
+
projection=projection, embed=embed)
|
|
290
|
+
.get("_embedded", {}).get("searchResult", {}))
|
|
291
|
+
objects = result.get("_embedded", {}).get("objects", [])
|
|
292
|
+
for obj in objects:
|
|
293
|
+
dso = obj.get("_embedded", {}).get("indexableObject")
|
|
294
|
+
if dso:
|
|
295
|
+
found.append(dso)
|
|
296
|
+
if max_results is not None and len(found) >= max_results:
|
|
297
|
+
return found
|
|
298
|
+
page += 1
|
|
299
|
+
if not objects or page >= (result.get("page") or {}).get("totalPages", 0):
|
|
300
|
+
return found
|
|
301
|
+
|
|
302
|
+
def get_by_href(self, href: str, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
303
|
+
"""Follow a HAL link, e.g. item['_links']['owningCollection']['href'].
|
|
304
|
+
|
|
305
|
+
Parameters:
|
|
306
|
+
href: The HAL link to follow.
|
|
307
|
+
all_languages: Whether to retrieve all language variants.
|
|
308
|
+
projection: Optional HAL projection parameter.
|
|
309
|
+
embed: Optional HAL embed parameter.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
The JSON response as a dictionary.
|
|
313
|
+
"""
|
|
314
|
+
return self._get_json(self.resolve_href(href), f"GET {href}", all_languages=all_languages,
|
|
315
|
+
projection=projection, embed=embed)
|
|
316
|
+
|
|
317
|
+
# ------------------------------------------------------------------
|
|
318
|
+
# References
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _uuid_of(ref) -> Optional[str]:
|
|
323
|
+
"""
|
|
324
|
+
UUID from a DSpace object dict, a UUID string or a HAL href.
|
|
325
|
+
Returns None when ref is something else (e.g. a name or an email).
|
|
326
|
+
|
|
327
|
+
Parameters:
|
|
328
|
+
ref: The reference to extract the UUID from. Can be a DSpace object dict, a UUID string, or a HAL href.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The UUID string if it can be determined, otherwise None.
|
|
332
|
+
"""
|
|
333
|
+
if isinstance(ref, dict):
|
|
334
|
+
return ref.get("uuid") or ref.get("id")
|
|
335
|
+
if isinstance(ref, str):
|
|
336
|
+
if validate_uuid(ref):
|
|
337
|
+
return ref
|
|
338
|
+
if ref.startswith(("http://", "https://", "/")):
|
|
339
|
+
return extract_uuid_from_url(ref)
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def _format_metadata_field(values):
|
|
344
|
+
"""Normalise a str / dict / list into a DSpace MetadataValue list.
|
|
345
|
+
|
|
346
|
+
Parameters:
|
|
347
|
+
values: The metadata value(s) to normalise.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
A list of DSpace MetadataValue dictionaries.
|
|
351
|
+
"""
|
|
352
|
+
if isinstance(values, str):
|
|
353
|
+
return [{"value": values}]
|
|
354
|
+
if isinstance(values, dict):
|
|
355
|
+
return [values]
|
|
356
|
+
if isinstance(values, list):
|
|
357
|
+
return [v if isinstance(v, dict) else {"value": str(v)} for v in values]
|
|
358
|
+
return [{"value": str(values)}]
|
|
359
|
+
|
|
360
|
+
def _format_metadata(self, metadata: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
361
|
+
"""Format the metadata dictionary into the structure expected by DSpace.
|
|
362
|
+
|
|
363
|
+
Parameters:
|
|
364
|
+
metadata: The metadata dictionary to format.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
The formatted metadata dictionary.
|
|
368
|
+
"""
|
|
369
|
+
return {field: self._format_metadata_field(v) for field, v in (metadata or {}).items()}
|
|
370
|
+
|
|
371
|
+
def _metadata_patch_ops(self, operation: str, metadata: Dict[str, Any]):
|
|
372
|
+
"""Generate JSON Patch operations for modifying metadata.
|
|
373
|
+
|
|
374
|
+
Parameters:
|
|
375
|
+
operation: The JSON Patch operation ('add', 'replace', or 'remove').
|
|
376
|
+
metadata: The metadata dictionary to modify.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
A list of JSON Patch operations for the given metadata modification.
|
|
380
|
+
"""
|
|
381
|
+
if operation not in ("add", "replace", "remove"):
|
|
382
|
+
raise ValueError("operation must be 'add', 'replace' or 'remove'")
|
|
383
|
+
ops = []
|
|
384
|
+
for field, values in metadata.items():
|
|
385
|
+
op = {"op": operation, "path": f"/metadata/{field}"}
|
|
386
|
+
if operation != "remove":
|
|
387
|
+
op["value"] = self._format_metadata_field(values)
|
|
388
|
+
ops.append(op)
|
|
389
|
+
return ops
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def _upsert_ops(existing: Dict[str, Any], metadata: Dict[str, Any], formatter):
|
|
393
|
+
"""Generate JSON Patch operations to upsert metadata fields.
|
|
394
|
+
|
|
395
|
+
For fields that already exist in the object, use 'replace'.
|
|
396
|
+
For new fields, use 'add'.
|
|
397
|
+
|
|
398
|
+
Parameters:
|
|
399
|
+
existing: The existing metadata dictionary of the object.
|
|
400
|
+
metadata: The new metadata to upsert.
|
|
401
|
+
formatter: A function to format the metadata values.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
A list of JSON Patch operations for upserting the metadata.
|
|
405
|
+
"""
|
|
406
|
+
return [{"op": "replace" if existing.get(f) else "add", "path": f"/metadata/{f}", "value": formatter(v)}
|
|
407
|
+
for f, v in metadata.items()]
|