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/versions.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Item versioning for DSpace API (DSpace 7+ / DSpace-CRIS)
|
|
4
|
+
|
|
5
|
+
POST /versioning/versions[?summary=] text/uri-list: the item -> new version (draft)
|
|
6
|
+
GET /versioning/versions/{id}[/item|/versionhistory]
|
|
7
|
+
PATCH /versioning/versions/{id} /summary only
|
|
8
|
+
GET /versioning/versionhistories/{id}[/versions|/draftVersion]
|
|
9
|
+
GET /core/items/{uuid}/version 204 when the item is not versioned
|
|
10
|
+
|
|
11
|
+
Creating a version copies the item into a new workspace item (the draft);
|
|
12
|
+
it becomes the latest version once deposited (and approved, if the
|
|
13
|
+
collection has a workflow). new_version() does all of that:
|
|
14
|
+
|
|
15
|
+
v = client.versions
|
|
16
|
+
result = v.new_version(item_uuid, "Fixed author order",
|
|
17
|
+
metadata={"dc.contributor.author": ["Roe, Richard", "Doe, Jane"]})
|
|
18
|
+
result["item_uuid"] # the new version's item
|
|
19
|
+
v.list_item_versions(item_uuid) # newest first
|
|
20
|
+
|
|
21
|
+
Deleting a version means deleting its item (archived) or its draft workspace
|
|
22
|
+
item - discard_draft() does the latter. Version histories may require admin
|
|
23
|
+
rights (versioning.item.history.view.admin), and entity items can only be
|
|
24
|
+
versioned when versioning.block.entity = false.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any, Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from ._base import BaseManager
|
|
31
|
+
from .exceptions import DSpaceError
|
|
32
|
+
from .projection import EmbedArg
|
|
33
|
+
|
|
34
|
+
# Logger for the module
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# API endpoints for item versioning and version histories
|
|
38
|
+
VERSIONS = "/versioning/versions"
|
|
39
|
+
VERSION_HISTORIES = "/versioning/versionhistories"
|
|
40
|
+
ITEMS = "/core/items"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class VersionManager(BaseManager):
|
|
44
|
+
"""Item versions and version histories"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, auth):
|
|
47
|
+
"""Initialize the VersionManager with authentication."""
|
|
48
|
+
super().__init__(auth)
|
|
49
|
+
self._workspace = None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def workspace(self):
|
|
53
|
+
"""Get the workspace manager, initializing it if necessary."""
|
|
54
|
+
if self._workspace is None:
|
|
55
|
+
from .workspaces import WorkspaceManager
|
|
56
|
+
self._workspace = WorkspaceManager(self.auth)
|
|
57
|
+
return self._workspace
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
# Versions
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def get_version(self, version_id, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
64
|
+
"""Get the details of a specific version.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
version_id: The ID of the version
|
|
68
|
+
embed: Optional embedding argument
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dict[str, Any]: The version details
|
|
72
|
+
"""
|
|
73
|
+
return self._get_json(self._url(VERSIONS, f"/{version_id}"), f"Get version {version_id}", embed=embed)
|
|
74
|
+
|
|
75
|
+
def get_item_version(self, item_uuid: str, embed: EmbedArg = None) -> Optional[Dict[str, Any]]:
|
|
76
|
+
"""Get the version an item represents, or None if the item is not versioned.
|
|
77
|
+
|
|
78
|
+
Parameters:
|
|
79
|
+
item_uuid: The UUID of the item
|
|
80
|
+
embed: Optional embedding argument
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Optional[Dict[str, Any]]: The version details or None if not versioned
|
|
84
|
+
"""
|
|
85
|
+
return self._get_optional(self._url(ITEMS, f"/{item_uuid}/version"), f"Get version of item {item_uuid}",
|
|
86
|
+
embed=embed)
|
|
87
|
+
|
|
88
|
+
def get_version_item(self, version_id, all_languages: Optional[bool] = None) -> Dict[str, Any]:
|
|
89
|
+
"""Get the item corresponding to a specific version.
|
|
90
|
+
|
|
91
|
+
Parameters:
|
|
92
|
+
version_id: The ID of the version
|
|
93
|
+
all_languages: Optional flag to retrieve all language variants
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict[str, Any]: The item details
|
|
97
|
+
"""
|
|
98
|
+
return self._get_json(self._url(VERSIONS, f"/{version_id}/item"), f"Get item of version {version_id}",
|
|
99
|
+
all_languages=all_languages)
|
|
100
|
+
|
|
101
|
+
def create_version(self, item_uuid: str, summary: Optional[str] = None) -> Dict[str, Any]:
|
|
102
|
+
"""Create a new version of an archived item.
|
|
103
|
+
|
|
104
|
+
New version of an archived item (admins, or the submitter if
|
|
105
|
+
versioning.submitterCanCreateNewVersion). The new item starts as a
|
|
106
|
+
workspace item - see new_version() for the complete flow.
|
|
107
|
+
422: a draft version already exists in this history.
|
|
108
|
+
|
|
109
|
+
Parameters:
|
|
110
|
+
item_uuid: The UUID of the item
|
|
111
|
+
summary: Optional summary for the new version
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dict[str, Any]: The created version details
|
|
115
|
+
"""
|
|
116
|
+
params = {"summary": summary} if summary else None
|
|
117
|
+
version = self._request("POST", self._url(VERSIONS), f"Create version of item {item_uuid}",
|
|
118
|
+
expected=(200, 201), headers=self._headers("text/uri-list"), params=params,
|
|
119
|
+
data=self._url(ITEMS, f"/{item_uuid}")).json()
|
|
120
|
+
logger.info("Created version %s (no. %s) of item %s", version.get("id"), version.get("version"), item_uuid)
|
|
121
|
+
return version
|
|
122
|
+
|
|
123
|
+
def set_summary(self, version_id, summary: Optional[str]) -> Dict[str, Any]:
|
|
124
|
+
"""Set, replace or (summary=None) remove the version summary.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
version_id: The ID of the version
|
|
128
|
+
summary: The new summary, or None to remove it
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dict[str, Any]: The updated version details
|
|
132
|
+
"""
|
|
133
|
+
op = {"op": "remove", "path": "/summary"} if summary is None else \
|
|
134
|
+
{"op": "add", "path": "/summary", "value": summary}
|
|
135
|
+
return self._request("PATCH", self._url(VERSIONS, f"/{version_id}"), f"Update summary of version {version_id}",
|
|
136
|
+
headers=self._headers(), json=[op]).json()
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
# Histories
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def get_history(self, history_id) -> Dict[str, Any]:
|
|
143
|
+
"""Get the details of a specific version history.
|
|
144
|
+
|
|
145
|
+
Parameters:
|
|
146
|
+
history_id: The ID of the version history
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dict[str, Any]: The version history details
|
|
150
|
+
"""
|
|
151
|
+
return self._get_json(self._url(VERSION_HISTORIES, f"/{history_id}"), f"Get version history {history_id}")
|
|
152
|
+
|
|
153
|
+
def get_version_history(self, version_id) -> Dict[str, Any]:
|
|
154
|
+
"""Get the version history of a specific version.
|
|
155
|
+
|
|
156
|
+
Parameters:
|
|
157
|
+
version_id: The ID of the version
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dict[str, Any]: The version history details
|
|
161
|
+
"""
|
|
162
|
+
return self._get_json(self._url(VERSIONS, f"/{version_id}/versionhistory"),
|
|
163
|
+
f"Get history of version {version_id}")
|
|
164
|
+
|
|
165
|
+
def get_history_versions(self, history_id, embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
166
|
+
"""Get all versions of a specific version history, newest first (a draft is not included).
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
history_id: The ID of the version history
|
|
170
|
+
embed: Optional embedding argument
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List[Dict[str, Any]]: The list of versions in the history
|
|
174
|
+
"""
|
|
175
|
+
url = self._url(VERSION_HISTORIES, f"/{history_id}/versions")
|
|
176
|
+
return list(self._iter_embedded(url, "versions", f"List versions of history {history_id}", embed=embed))
|
|
177
|
+
|
|
178
|
+
def get_draft_version(self, history_id) -> Optional[Dict[str, Any]]:
|
|
179
|
+
"""Get the draft version of a specific version history, or None if there is no draft.
|
|
180
|
+
|
|
181
|
+
Parameters:
|
|
182
|
+
history_id: The ID of the version history
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Optional[Dict[str, Any]]: The draft version details or None if no draft exists
|
|
186
|
+
"""
|
|
187
|
+
return self._get_optional(self._url(VERSION_HISTORIES, f"/{history_id}/draftVersion"),
|
|
188
|
+
f"Get draft version of history {history_id}")
|
|
189
|
+
|
|
190
|
+
def history_of_item(self, item_uuid: str) -> Optional[Dict[str, Any]]:
|
|
191
|
+
"""Get the version history of the item corresponding to the given UUID.
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
item_uuid: The UUID of the item
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Optional[Dict[str, Any]]: The version history details or None if the item is not versioned
|
|
198
|
+
"""
|
|
199
|
+
version = self.get_item_version(item_uuid)
|
|
200
|
+
return self.get_version_history(version["id"]) if version else None
|
|
201
|
+
|
|
202
|
+
def list_item_versions(self, item_uuid: str, embed: EmbedArg = "item") -> List[Dict[str, Any]]:
|
|
203
|
+
"""All versions in the item's history, newest first ([] if not versioned).
|
|
204
|
+
Parameters:
|
|
205
|
+
item_uuid: The UUID of the item
|
|
206
|
+
embed: Optional embedding argument
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List[Dict[str, Any]]: The list of versions in the item's history
|
|
210
|
+
"""
|
|
211
|
+
history = self.history_of_item(item_uuid)
|
|
212
|
+
return self.get_history_versions(history["id"], embed=embed) if history else []
|
|
213
|
+
|
|
214
|
+
def latest_version(self, item_uuid: str) -> Optional[Dict[str, Any]]:
|
|
215
|
+
"""Get the latest version of the item, or None if not versioned.
|
|
216
|
+
|
|
217
|
+
Parameters:
|
|
218
|
+
item_uuid: The UUID of the item
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Optional[Dict[str, Any]]: The latest version details or None if not versioned
|
|
222
|
+
"""
|
|
223
|
+
versions = self.list_item_versions(item_uuid)
|
|
224
|
+
return versions[0] if versions else None
|
|
225
|
+
|
|
226
|
+
def is_latest(self, item_uuid: str) -> bool:
|
|
227
|
+
"""Check if the item is the newest archived version (or not versioned at all).
|
|
228
|
+
|
|
229
|
+
Parameters:
|
|
230
|
+
item_uuid: The UUID of the item
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
bool: True if the item is the newest archived version or not versioned, False otherwise
|
|
234
|
+
"""
|
|
235
|
+
own = self.get_item_version(item_uuid)
|
|
236
|
+
if not own:
|
|
237
|
+
return True
|
|
238
|
+
versions = self.get_history_versions(self.get_version_history(own["id"])["id"])
|
|
239
|
+
return not versions or str(versions[0].get("id")) == str(own.get("id"))
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Complete flows
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def new_version(self, item_uuid: str, summary: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None,
|
|
246
|
+
deposit: bool = True, strict_metadata: bool = True) -> Dict[str, Any]:
|
|
247
|
+
"""
|
|
248
|
+
Create a version, optionally change its metadata (routed to the
|
|
249
|
+
submission forms like WorkspaceManager.fill_metadata) and deposit it.
|
|
250
|
+
|
|
251
|
+
Returns {"version", "item_uuid", "workspace_item", "workflow_item"}.
|
|
252
|
+
On failure after the version was created the draft is kept, so it
|
|
253
|
+
can be inspected or removed with discard_draft().
|
|
254
|
+
|
|
255
|
+
Parameters:
|
|
256
|
+
item_uuid: The UUID of the item
|
|
257
|
+
summary: Optional summary for the new version
|
|
258
|
+
metadata: Optional metadata to update in the new version
|
|
259
|
+
deposit: Whether to deposit the new version immediately (default is True)
|
|
260
|
+
strict_metadata: Whether to enforce strict metadata validation (default is True)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Dict[str, Any]: Details of the newly created version, including the version, item UUID, workspace item, and workflow item
|
|
264
|
+
"""
|
|
265
|
+
version = self.create_version(item_uuid, summary)
|
|
266
|
+
new_item = self.get_version_item(version["id"])
|
|
267
|
+
ws = self.workspace.find_by_item(new_item["uuid"])
|
|
268
|
+
if ws is None:
|
|
269
|
+
raise DSpaceError(f"Version {version.get('id')} has no workspace item (already in workflow?)")
|
|
270
|
+
if metadata:
|
|
271
|
+
ws = self.workspace.fill_metadata(ws["id"], metadata, strict=strict_metadata)
|
|
272
|
+
workflow_item = self.workspace.deposit(ws["id"]) if deposit else None
|
|
273
|
+
return {"version": version, "item_uuid": new_item["uuid"], "workspace_item": ws,
|
|
274
|
+
"workflow_item": workflow_item}
|
|
275
|
+
|
|
276
|
+
def discard_draft(self, item_uuid: str) -> bool:
|
|
277
|
+
"""
|
|
278
|
+
Discard the draft version of an item, if it exists and is still a workspace item.
|
|
279
|
+
|
|
280
|
+
Parameters:
|
|
281
|
+
item_uuid: The UUID of the item
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
bool: True if the draft was successfully discarded, False if there was no draft
|
|
285
|
+
"""
|
|
286
|
+
history = self.history_of_item(item_uuid)
|
|
287
|
+
draft = self.get_draft_version(history["id"]) if history else None
|
|
288
|
+
if not draft:
|
|
289
|
+
return False
|
|
290
|
+
if draft.get("type") != "workspaceitem":
|
|
291
|
+
raise DSpaceError(f"The draft version is a {draft.get('type')} - only workspace items can be discarded")
|
|
292
|
+
return self.workspace.delete(draft["id"])
|
dspace_api/vocabulary.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Controlled vocabularies for DSpace API (DSpace 7+ / DSpace-CRIS)
|
|
4
|
+
|
|
5
|
+
GET /submission/vocabularies[/{name}]
|
|
6
|
+
GET /submission/vocabularies/{name}/entries?filter=&exact=&entryID=
|
|
7
|
+
GET /submission/vocabularies/search/byMetadataAndCollection?metadata=&collection=
|
|
8
|
+
GET /submission/vocabularyEntryDetails/{vocabulary}:{id}[/parent|/children]
|
|
9
|
+
GET /submission/vocabularyEntryDetails/search/top?vocabulary=
|
|
10
|
+
|
|
11
|
+
Vocabularies are value-pair lists from submission-forms.xml (common_types,
|
|
12
|
+
common_iso_languages), hierarchical XML vocabularies (srsc) and authorities
|
|
13
|
+
(SolrAuthorAuthority, ORCID, CRIS item authorities ...).
|
|
14
|
+
|
|
15
|
+
v = client.vocabulary
|
|
16
|
+
v.lookup("common_types", "Article") # exact entry or None
|
|
17
|
+
v.check_values("common_types", ["Article", "Artikel"]) # {"Article": entry, "Artikel": None}
|
|
18
|
+
v.vocabulary_for_field("dc.type", collection_uuid) # which vocabulary a form field uses
|
|
19
|
+
for depth, entry in v.iter_tree("srsc", max_depth=1): # hierarchical vocabularies
|
|
20
|
+
...
|
|
21
|
+
md = {"dc.subject": [v.to_metadata_value(entry)]} # value + authority + confidence
|
|
22
|
+
|
|
23
|
+
Everything here is readable anonymously (the server may restrict authorities).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
from ._base import BaseManager
|
|
30
|
+
from .config import Endpoints
|
|
31
|
+
|
|
32
|
+
# Logger for the module
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Endpoints for vocabularies and vocabulary entries
|
|
36
|
+
VOCABULARIES = Endpoints.VOCABULARIES # /submission/vocabularies
|
|
37
|
+
ENTRY_DETAILS = Endpoints.VOCABULARY_ENTRIES # /submission/vocabularyEntryDetails
|
|
38
|
+
|
|
39
|
+
# DSpace authority confidence for a value accepted from the authority (Choices.CF_ACCEPTED)
|
|
40
|
+
CONFIDENCE_ACCEPTED = 600
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detail_id(vocabulary: str, entry_id: str) -> str:
|
|
44
|
+
"""Id of an entry detail: ("srsc", "SCB110") -> "srsc:SCB110" (already prefixed ids are kept).
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
vocabulary: The vocabulary name
|
|
48
|
+
entry_id: The entry ID
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: The detail ID in the form "vocabulary:entry_id"
|
|
52
|
+
"""
|
|
53
|
+
entry_id = str(entry_id)
|
|
54
|
+
return entry_id if entry_id.startswith(f"{vocabulary}:") else f"{vocabulary}:{entry_id}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def has_children(entry: Dict[str, Any]) -> Optional[bool]:
|
|
58
|
+
"""otherInformation.hasChildren as bool, None if the server does not say.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
entry: The vocabulary entry
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Optional[bool]: True if the entry has children, False if not, None if unknown
|
|
65
|
+
"""
|
|
66
|
+
value = (entry.get("otherInformation") or {}).get("hasChildren")
|
|
67
|
+
return None if value is None else str(value).lower() == "true"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class VocabularyManager(BaseManager):
|
|
71
|
+
"""Controlled vocabularies, authorities and hierarchical vocabulary trees"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, auth):
|
|
74
|
+
"""Initialize the vocabulary manager with authentication."""
|
|
75
|
+
super().__init__(auth)
|
|
76
|
+
self._lookup_cache: Dict[Tuple[str, str, bool], Optional[Dict[str, Any]]] = {}
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# Vocabularies
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def get_vocabularies(self) -> List[Dict[str, Any]]:
|
|
83
|
+
"""List of vocabularies with their properties.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List[Dict[str, Any]]: Each dictionary contains keys "id", "name", "scrollable", "hierarchical", "preloadLevel"
|
|
87
|
+
"""
|
|
88
|
+
return list(self._iter_embedded(self._url(VOCABULARIES), "vocabularies", "List vocabularies"))
|
|
89
|
+
|
|
90
|
+
def get_vocabulary(self, name: str) -> Dict[str, Any]:
|
|
91
|
+
"""Get a specific vocabulary by name.
|
|
92
|
+
|
|
93
|
+
Parameters:
|
|
94
|
+
name: The name of the vocabulary
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dict[str, Any]: The vocabulary details
|
|
98
|
+
"""
|
|
99
|
+
return self._get_json(self._url(VOCABULARIES, f"/{name}"), f"Get vocabulary {name}")
|
|
100
|
+
|
|
101
|
+
def vocabulary_for_field(self, metadata_field: str, collection_uuid: str) -> Optional[Dict[str, Any]]:
|
|
102
|
+
"""The vocabulary a submission form uses for a field in a collection, or None.
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
metadata_field: The metadata field
|
|
106
|
+
collection_uuid: The UUID of the collection
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Optional[Dict[str, Any]]: The vocabulary details or None if not found
|
|
110
|
+
"""
|
|
111
|
+
return self._get_optional(self._url(VOCABULARIES, "/search/byMetadataAndCollection"),
|
|
112
|
+
f"Find vocabulary for {metadata_field}",
|
|
113
|
+
params={"metadata": metadata_field, "collection": collection_uuid})
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# Entries
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def iter_entries(self, name: str, filter: Optional[str] = None, exact: bool = False,
|
|
120
|
+
entry_id: Optional[str] = None, page_size: int = 100,
|
|
121
|
+
max_results: Optional[int] = None) -> Iterator[Dict[str, Any]]:
|
|
122
|
+
"""
|
|
123
|
+
Entries ({"display", "value", "authority"?, "otherInformation"?}) of a
|
|
124
|
+
vocabulary. Without filter / entry_id only scrollable vocabularies list
|
|
125
|
+
their entries (the server answers 422 otherwise). A generator.
|
|
126
|
+
|
|
127
|
+
Parameters:
|
|
128
|
+
name: The name of the vocabulary
|
|
129
|
+
filter: Optional filter string
|
|
130
|
+
exact: Whether to match the filter exactly
|
|
131
|
+
entry_id: Specific entry ID to retrieve
|
|
132
|
+
page_size: Number of entries per page
|
|
133
|
+
max_results: Maximum number of results to return
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Iterator[Dict[str, Any]]: The vocabulary entries
|
|
137
|
+
"""
|
|
138
|
+
if filter is not None and entry_id is not None:
|
|
139
|
+
raise ValueError("Use filter or entry_id, not both")
|
|
140
|
+
if exact and filter is None:
|
|
141
|
+
raise ValueError("exact=True needs a filter")
|
|
142
|
+
params: List[Tuple[str, Any]] = []
|
|
143
|
+
if filter is not None:
|
|
144
|
+
params.append(("filter", filter))
|
|
145
|
+
if exact:
|
|
146
|
+
params.append(("exact", "true"))
|
|
147
|
+
if entry_id is not None:
|
|
148
|
+
params.append(("entryID", entry_id))
|
|
149
|
+
return self._iter_embedded(self._url(VOCABULARIES, f"/{name}/entries"), "entries",
|
|
150
|
+
f"Get entries of vocabulary {name}", params, page_size=page_size,
|
|
151
|
+
max_results=max_results)
|
|
152
|
+
|
|
153
|
+
def get_entries(self, name: str, filter: Optional[str] = None, exact: bool = False,
|
|
154
|
+
entry_id: Optional[str] = None, max_results: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
155
|
+
"""
|
|
156
|
+
Get vocabulary entries as a list.
|
|
157
|
+
|
|
158
|
+
Parameters:
|
|
159
|
+
name: The name of the vocabulary
|
|
160
|
+
filter: Optional filter string
|
|
161
|
+
exact: Whether to match the filter exactly
|
|
162
|
+
entry_id: Specific entry ID to retrieve
|
|
163
|
+
max_results: Maximum number of results to return
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List[Dict[str, Any]]: The vocabulary entries
|
|
167
|
+
"""
|
|
168
|
+
return list(self.iter_entries(name, filter, exact, entry_id, max_results=max_results))
|
|
169
|
+
|
|
170
|
+
def search(self, name: str, text: str, max_results: int = 20) -> List[Dict[str, Any]]:
|
|
171
|
+
"""Suggestions for user input (prefix / keyword match, as in the submission form).
|
|
172
|
+
|
|
173
|
+
Parameters:
|
|
174
|
+
name: The name of the vocabulary
|
|
175
|
+
text: The text to search for
|
|
176
|
+
max_results: Maximum number of results to return
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List[Dict[str, Any]]: The matching vocabulary entries
|
|
180
|
+
"""
|
|
181
|
+
return self.get_entries(name, filter=text, max_results=max_results)
|
|
182
|
+
|
|
183
|
+
def lookup(self, name: str, value: str, match_display: bool = True) -> Optional[Dict[str, Any]]:
|
|
184
|
+
"""
|
|
185
|
+
The entry whose stored value (or, with match_display, display label)
|
|
186
|
+
is exactly `value`, or None. Results are cached per manager.
|
|
187
|
+
|
|
188
|
+
Parameters:
|
|
189
|
+
name: The name of the vocabulary
|
|
190
|
+
value: The value to look up
|
|
191
|
+
match_display: Whether to match against the display label as well
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Optional[Dict[str, Any]]: The matching vocabulary entry or None if not found
|
|
195
|
+
"""
|
|
196
|
+
key = (name, value, bool(match_display))
|
|
197
|
+
if key not in self._lookup_cache:
|
|
198
|
+
candidates = self.get_entries(name, filter=value, exact=True, max_results=50)
|
|
199
|
+
match = next((e for e in candidates if e.get("value") == value), None)
|
|
200
|
+
if match is None and match_display:
|
|
201
|
+
match = next((e for e in candidates if e.get("display") == value), None)
|
|
202
|
+
self._lookup_cache[key] = match
|
|
203
|
+
return self._lookup_cache[key]
|
|
204
|
+
|
|
205
|
+
def check_values(self, name: str, values: Iterable[str]) -> Dict[str, Optional[Dict[str, Any]]]:
|
|
206
|
+
"""{value: entry or None} - validate import data against a vocabulary.
|
|
207
|
+
|
|
208
|
+
Parameters:
|
|
209
|
+
name: The name of the vocabulary
|
|
210
|
+
values: The values to check
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dict[str, Optional[Dict[str, Any]]]: Mapping of values to their corresponding entries or None if not found
|
|
214
|
+
"""
|
|
215
|
+
return {value: self.lookup(name, value) for value in dict.fromkeys(values)}
|
|
216
|
+
|
|
217
|
+
def invalid_values(self, name: str, values: Iterable[str]) -> List[str]:
|
|
218
|
+
"""The values (in order, no duplicates) that the vocabulary does not contain.
|
|
219
|
+
|
|
220
|
+
Parameters:
|
|
221
|
+
name: The name of the vocabulary
|
|
222
|
+
values: The values to check
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List[str]: The values not found in the vocabulary
|
|
226
|
+
"""
|
|
227
|
+
return [value for value, entry in self.check_values(name, values).items() if entry is None]
|
|
228
|
+
|
|
229
|
+
def get_entry_by_id(self, name: str, entry_id: str) -> Optional[Dict[str, Any]]:
|
|
230
|
+
"""Entry (as in iter_entries) by its id within the vocabulary, or None.
|
|
231
|
+
|
|
232
|
+
Parameters:
|
|
233
|
+
name: The name of the vocabulary
|
|
234
|
+
entry_id: The ID of the entry to retrieve
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Optional[Dict[str, Any]]: The entry details or None if not found
|
|
238
|
+
"""
|
|
239
|
+
entries = self.get_entries(name, entry_id=entry_id, max_results=1)
|
|
240
|
+
return entries[0] if entries else None
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def to_metadata_value(entry: Dict[str, Any], language: Optional[str] = None,
|
|
244
|
+
confidence: int = CONFIDENCE_ACCEPTED) -> Dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
A MetadataValue dict for an entry: the stored value plus, for
|
|
247
|
+
authorities, the authority key and confidence (600 = accepted).
|
|
248
|
+
|
|
249
|
+
Parameters:
|
|
250
|
+
entry: The vocabulary entry
|
|
251
|
+
language: Optional language code for the metadata value
|
|
252
|
+
confidence: Confidence level for the authority (default is CONFIDENCE_ACCEPTED)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Dict[str, Any]: The metadata value dictionary
|
|
256
|
+
"""
|
|
257
|
+
value: Dict[str, Any] = {"value": entry.get("value") or entry.get("display")}
|
|
258
|
+
if entry.get("authority"):
|
|
259
|
+
value["authority"] = entry["authority"]
|
|
260
|
+
value["confidence"] = confidence
|
|
261
|
+
if language:
|
|
262
|
+
value["language"] = language
|
|
263
|
+
return value
|
|
264
|
+
|
|
265
|
+
# ------------------------------------------------------------------
|
|
266
|
+
# Entry details and hierarchies
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def get_entry_detail(self, vocabulary: str, entry_id: str) -> Dict[str, Any]:
|
|
270
|
+
"""Get the details of a vocabulary entry.
|
|
271
|
+
{"id": "srsc:SCB110", "value", "display", "selectable", "otherInformation"}
|
|
272
|
+
|
|
273
|
+
Parameters:
|
|
274
|
+
vocabulary: The name of the vocabulary
|
|
275
|
+
entry_id: The ID of the entry to retrieve
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Dict[str, Any]: The entry details
|
|
279
|
+
"""
|
|
280
|
+
ident = detail_id(vocabulary, entry_id)
|
|
281
|
+
return self._get_json(self._url(ENTRY_DETAILS, f"/{ident}"), f"Get vocabulary entry {ident}")
|
|
282
|
+
|
|
283
|
+
def get_top_entries(self, vocabulary: str) -> List[Dict[str, Any]]:
|
|
284
|
+
"""Root entries of a hierarchical vocabulary (422 for flat ones).
|
|
285
|
+
|
|
286
|
+
Parameters:
|
|
287
|
+
vocabulary: The name of the vocabulary
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List[Dict[str, Any]]: The top entries of the vocabulary
|
|
291
|
+
"""
|
|
292
|
+
return list(self._iter_embedded(self._url(ENTRY_DETAILS, "/search/top"),
|
|
293
|
+
("vocabularyEntryDetails", "entries"), f"Top entries of {vocabulary}",
|
|
294
|
+
[("vocabulary", vocabulary)]))
|
|
295
|
+
|
|
296
|
+
def get_children(self, vocabulary: str, entry_id: str) -> List[Dict[str, Any]]:
|
|
297
|
+
ident = detail_id(vocabulary, entry_id)
|
|
298
|
+
"""Get the children of a vocabulary entry.
|
|
299
|
+
|
|
300
|
+
Parameters:
|
|
301
|
+
vocabulary: The name of the vocabulary
|
|
302
|
+
entry_id: The ID of the entry
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List[Dict[str, Any]]: The child entries of the vocabulary entry
|
|
306
|
+
"""
|
|
307
|
+
return list(self._iter_embedded(self._url(ENTRY_DETAILS, f"/{ident}/children"),
|
|
308
|
+
("children", "vocabularyEntryDetails"), f"Children of {ident}"))
|
|
309
|
+
|
|
310
|
+
def get_parent(self, vocabulary: str, entry_id: str) -> Optional[Dict[str, Any]]:
|
|
311
|
+
"""The parent entry detail, None for root entries.
|
|
312
|
+
|
|
313
|
+
Parameters:
|
|
314
|
+
vocabulary: The name of the vocabulary
|
|
315
|
+
entry_id: The ID of the entry
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Optional[Dict[str, Any]]: The parent entry details or None if root
|
|
319
|
+
"""
|
|
320
|
+
ident = detail_id(vocabulary, entry_id)
|
|
321
|
+
return self._get_optional(self._url(ENTRY_DETAILS, f"/{ident}/parent"), f"Parent of {ident}")
|
|
322
|
+
|
|
323
|
+
def get_path(self, vocabulary: str, entry_id: str, max_depth: int = 50) -> List[Dict[str, Any]]:
|
|
324
|
+
"""Entry details from the root down to the entry itself.
|
|
325
|
+
|
|
326
|
+
Parameters:
|
|
327
|
+
vocabulary: The name of the vocabulary
|
|
328
|
+
entry_id: The ID of the entry
|
|
329
|
+
max_depth: Maximum depth to traverse (default is 50)
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
List[Dict[str, Any]]: The path from the root to the entry
|
|
333
|
+
"""
|
|
334
|
+
path = [self.get_entry_detail(vocabulary, entry_id)]
|
|
335
|
+
while len(path) < max_depth:
|
|
336
|
+
parent = self.get_parent(vocabulary, path[0]["id"])
|
|
337
|
+
if not parent:
|
|
338
|
+
break
|
|
339
|
+
path.insert(0, parent)
|
|
340
|
+
return path
|
|
341
|
+
|
|
342
|
+
def iter_tree(self, vocabulary: str, max_depth: Optional[int] = None,
|
|
343
|
+
root_id: Optional[str] = None) -> Iterator[Tuple[int, Dict[str, Any]]]:
|
|
344
|
+
"""
|
|
345
|
+
Depth-first walk of a hierarchical vocabulary: yields (depth, entry
|
|
346
|
+
detail), from the top entries (or below root_id). Children are only
|
|
347
|
+
requested where otherInformation.hasChildren is not "false".
|
|
348
|
+
|
|
349
|
+
Parameters:
|
|
350
|
+
vocabulary: The name of the vocabulary
|
|
351
|
+
max_depth: Optional maximum depth to traverse
|
|
352
|
+
root_id: Optional ID of the root entry to start from
|
|
353
|
+
|
|
354
|
+
Yields:
|
|
355
|
+
Tuple[int, Dict[str, Any]]: A tuple of (depth, entry detail) for each entry in the tree
|
|
356
|
+
"""
|
|
357
|
+
roots = self.get_children(vocabulary, root_id) if root_id else self.get_top_entries(vocabulary)
|
|
358
|
+
stack = [(0, entry) for entry in reversed(roots)]
|
|
359
|
+
while stack:
|
|
360
|
+
depth, entry = stack.pop()
|
|
361
|
+
yield depth, entry
|
|
362
|
+
if (max_depth is not None and depth >= max_depth) or has_children(entry) is False:
|
|
363
|
+
continue
|
|
364
|
+
for child in reversed(self.get_children(vocabulary, entry["id"])):
|
|
365
|
+
stack.append((depth + 1, child))
|