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/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"])
@@ -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))