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/projection.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Projections and embeds for DSpace REST requests.
|
|
4
|
+
|
|
5
|
+
DSpace lets every HAL GET request shape its response (RestContract
|
|
6
|
+
projections.md):
|
|
7
|
+
|
|
8
|
+
projection=full every link embedded, max. two levels
|
|
9
|
+
projection=allLanguages metadata in every language (DSpace-CRIS)
|
|
10
|
+
embed=bundles embed one linked resource
|
|
11
|
+
embed=bundles/bitstreams nested: the bundles AND their bitstreams
|
|
12
|
+
embed=bundles&embed=owningCollection several (or comma separated)
|
|
13
|
+
embed.size=bundles=50 page size of an embedded list
|
|
14
|
+
|
|
15
|
+
Several projections can be combined by repeating the parameter; DSpace
|
|
16
|
+
builds a composite projection (Utils.obtainProjection). embed.size is not
|
|
17
|
+
described on the contract page but is supported by the server; without it
|
|
18
|
+
an embedded list holds only its default page (usually 20) - use
|
|
19
|
+
get_embedded_page() / is_embedded_complete() to detect truncation.
|
|
20
|
+
|
|
21
|
+
The toolkit accepts these per call on its read methods:
|
|
22
|
+
|
|
23
|
+
client.items.get_item(uuid, embed=["bundles/bitstreams", "owningCollection"])
|
|
24
|
+
client.items.get_item(uuid, embed={"bundles": 50, "bundles/bitstreams": 200})
|
|
25
|
+
client.collections.get_collection(uuid, projection="full")
|
|
26
|
+
|
|
27
|
+
and the helpers below read the result:
|
|
28
|
+
|
|
29
|
+
get_embedded(item, "owningCollection") -> dict or None
|
|
30
|
+
get_embedded(item, "bundles/bitstreams") -> flat list of bitstreams
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
|
|
34
|
+
|
|
35
|
+
# Standard projection and embed constants and normalization functions for DSpace REST API requests.
|
|
36
|
+
FULL = "full"
|
|
37
|
+
ALL_LANGUAGES = "allLanguages"
|
|
38
|
+
|
|
39
|
+
# Accepted argument shapes
|
|
40
|
+
EmbedArg = Union[None, str, Iterable[str], Mapping[str, Optional[int]]]
|
|
41
|
+
ProjectionArg = Union[None, str, Iterable[str]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _split(value) -> List[str]:
|
|
45
|
+
"""Split a comma-separated string or iterable into a list of non-empty, stripped strings."""
|
|
46
|
+
if value is None:
|
|
47
|
+
return []
|
|
48
|
+
if isinstance(value, str):
|
|
49
|
+
value = value.split(",")
|
|
50
|
+
return [v.strip() for v in value if v and str(v).strip()]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_projection(projection: ProjectionArg) -> List[str]:
|
|
54
|
+
"""Normalize a projection argument into a list of unique projection names.
|
|
55
|
+
|
|
56
|
+
'full' / 'full,allLanguages' / ['full'] -> ['full', ...] (order kept, no duplicates).
|
|
57
|
+
"""
|
|
58
|
+
seen: List[str] = []
|
|
59
|
+
for name in _split(projection):
|
|
60
|
+
if name not in seen:
|
|
61
|
+
seen.append(name)
|
|
62
|
+
return seen
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _clean_path(path: str) -> str:
|
|
66
|
+
"""Clean and validate an embed path, ensuring it is non-empty and properly formatted."""
|
|
67
|
+
path = str(path).strip().strip("/")
|
|
68
|
+
if not path or any(not part.strip() for part in path.split("/")):
|
|
69
|
+
raise ValueError(f"Invalid embed path {path!r}")
|
|
70
|
+
return "/".join(part.strip() for part in path.split("/"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def normalize_embed(embed: EmbedArg) -> Tuple[List[str], Dict[str, int]]:
|
|
74
|
+
"""Normalize an embed argument into a list of unique embed paths and a dictionary of embed sizes.
|
|
75
|
+
|
|
76
|
+
-> (embed paths, embed sizes)
|
|
77
|
+
|
|
78
|
+
embed may be a string ("bundles,owningCollection"), a list of paths, or a
|
|
79
|
+
dict {path: size or None}. A size applies to that embedded list.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
embed (EmbedArg): The embed argument to normalize.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Tuple[List[str], Dict[str, int]]: A tuple containing the list of unique embed paths and a dictionary of embed sizes.
|
|
86
|
+
"""
|
|
87
|
+
if embed is None:
|
|
88
|
+
return [], {}
|
|
89
|
+
sizes: Dict[str, int] = {}
|
|
90
|
+
if isinstance(embed, Mapping):
|
|
91
|
+
paths = []
|
|
92
|
+
for path, size in embed.items():
|
|
93
|
+
clean = _clean_path(path)
|
|
94
|
+
paths.append(clean)
|
|
95
|
+
if size is not None:
|
|
96
|
+
if isinstance(size, bool) or not isinstance(size, int) or size < 1:
|
|
97
|
+
raise ValueError(f"embed size for {path!r} must be a positive int, not {size!r}")
|
|
98
|
+
sizes[clean] = size
|
|
99
|
+
else:
|
|
100
|
+
paths = [_clean_path(p) for p in _split(embed)]
|
|
101
|
+
unique: List[str] = []
|
|
102
|
+
for path in paths:
|
|
103
|
+
if path not in unique:
|
|
104
|
+
unique.append(path)
|
|
105
|
+
return unique, sizes
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _merge(params, extra: Sequence[Tuple[str, str]]):
|
|
109
|
+
"""Add (key, value) pairs to params (None / dict / list of tuples), skipping exact duplicates.
|
|
110
|
+
|
|
111
|
+
Parameters:
|
|
112
|
+
params (None | dict | list of tuples): The existing parameters to merge into.
|
|
113
|
+
extra (Sequence[Tuple[str, str]]): The additional (key, value) pairs to add.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The merged parameters in the same shape as the input `params`.
|
|
117
|
+
"""
|
|
118
|
+
if not extra:
|
|
119
|
+
return params
|
|
120
|
+
if params is None:
|
|
121
|
+
params = []
|
|
122
|
+
if isinstance(params, Mapping):
|
|
123
|
+
merged = dict(params)
|
|
124
|
+
for key, value in extra:
|
|
125
|
+
current = merged.get(key)
|
|
126
|
+
if current is None:
|
|
127
|
+
merged[key] = value
|
|
128
|
+
continue
|
|
129
|
+
values = list(current) if isinstance(current, (list, tuple)) else [current]
|
|
130
|
+
if value not in values:
|
|
131
|
+
values.append(value)
|
|
132
|
+
merged[key] = values if len(values) > 1 else values[0]
|
|
133
|
+
return merged
|
|
134
|
+
items = list(params)
|
|
135
|
+
for pair in extra:
|
|
136
|
+
if pair not in items:
|
|
137
|
+
items.append(pair)
|
|
138
|
+
return items
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def apply_view(params, projection: ProjectionArg = None, embed: EmbedArg = None):
|
|
142
|
+
"""
|
|
143
|
+
Return params with projection / embed / embed.size added, in the same
|
|
144
|
+
shape that was passed (None becomes a list of tuples). Existing values
|
|
145
|
+
(e.g. projection=preventMetadataSecurity, embed=group) are kept.
|
|
146
|
+
|
|
147
|
+
Parameters:
|
|
148
|
+
params (None | dict | list of tuples): The existing parameters to merge into.
|
|
149
|
+
projection (ProjectionArg, optional): The projection argument to apply.
|
|
150
|
+
embed (EmbedArg, optional): The embed argument to apply.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The merged parameters with the projection and embed applied, in the same shape as the input `params`.
|
|
154
|
+
"""
|
|
155
|
+
paths, sizes = normalize_embed(embed)
|
|
156
|
+
extra = [("projection", p) for p in normalize_projection(projection)]
|
|
157
|
+
extra += [("embed", p) for p in paths]
|
|
158
|
+
extra += [("embed.size", f"{p}={n}") for p, n in sizes.items()]
|
|
159
|
+
return _merge(params, extra)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def view_params(params, config=None, all_languages: Optional[bool] = None,
|
|
163
|
+
projection: ProjectionArg = None, embed: EmbedArg = None):
|
|
164
|
+
"""
|
|
165
|
+
params + the caller's projection/embed + allLanguages if the config
|
|
166
|
+
(per-call value > languages() scope > default) asks for it.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
params (None | dict | list of tuples): The existing parameters to merge into.
|
|
170
|
+
config (optional): The configuration object that may influence the allLanguages setting.
|
|
171
|
+
all_languages (Optional[bool], optional): Whether to include all languages.
|
|
172
|
+
projection (ProjectionArg, optional): The projection argument to apply.
|
|
173
|
+
embed (EmbedArg, optional): The embed argument to apply.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The merged parameters with the projection and embed applied, including allLanguages if requested.
|
|
177
|
+
"""
|
|
178
|
+
resolve = getattr(config, "use_all_languages", None)
|
|
179
|
+
wants_all = resolve(all_languages) if resolve else bool(all_languages)
|
|
180
|
+
projections = normalize_projection(projection)
|
|
181
|
+
if wants_all and ALL_LANGUAGES not in projections:
|
|
182
|
+
projections.append(ALL_LANGUAGES)
|
|
183
|
+
return apply_view(params, projections, embed)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Reading embedded results
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def _is_page(value) -> bool:
|
|
191
|
+
"""Check if the given value represents a paginated embedded resource.
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
value (Any): The value to check.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
bool: True if the value represents a paginated embedded resource, False otherwise.
|
|
198
|
+
"""
|
|
199
|
+
return isinstance(value, dict) and "page" in value and isinstance(value.get("_embedded"), dict)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _page_items(page: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
203
|
+
"""Extract the list of items from a paginated embedded resource.
|
|
204
|
+
Parameters:
|
|
205
|
+
page (Dict[str, Any]): The paginated embedded resource.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
List[Dict[str, Any]]: The list of items contained in the paginated resource, or an empty list if none are found.
|
|
209
|
+
"""
|
|
210
|
+
for value in page["_embedded"].values():
|
|
211
|
+
if isinstance(value, list):
|
|
212
|
+
return value
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_embedded_page(resource: Optional[Dict[str, Any]], rel: str
|
|
217
|
+
) -> Tuple[Optional[List[Dict[str, Any]]], Optional[int]]:
|
|
218
|
+
"""
|
|
219
|
+
Get the embedded page for the given relationship key `rel` from the `resource`.
|
|
220
|
+
|
|
221
|
+
(items, totalElements) of an embedded list on `resource`, or (None, None)
|
|
222
|
+
if it was not embedded. totalElements is None when DSpace sent no page info.
|
|
223
|
+
|
|
224
|
+
Parameters:
|
|
225
|
+
resource (Optional[Dict[str, Any]]): The resource containing the embedded list.
|
|
226
|
+
rel (str): The relationship key for the embedded list.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tuple[Optional[List[Dict[str, Any]]], Optional[int]]: A tuple containing the list of embedded items and the total number of elements, or (None, None) if not embedded.
|
|
230
|
+
"""
|
|
231
|
+
value = ((resource or {}).get("_embedded") or {}).get(rel)
|
|
232
|
+
if value is None:
|
|
233
|
+
return None, None
|
|
234
|
+
if isinstance(value, list):
|
|
235
|
+
return value, None
|
|
236
|
+
if _is_page(value):
|
|
237
|
+
return _page_items(value), (value.get("page") or {}).get("totalElements")
|
|
238
|
+
return None, None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def is_embedded_complete(resource: Optional[Dict[str, Any]], rel: str) -> bool:
|
|
242
|
+
"""True if the embedded list `rel` holds all its elements (not truncated by paging).
|
|
243
|
+
|
|
244
|
+
Parameters:
|
|
245
|
+
resource (Optional[Dict[str, Any]]): The resource containing the embedded list.
|
|
246
|
+
rel (str): The relationship key for the embedded list.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
bool: True if the embedded list holds all its elements, False otherwise.
|
|
250
|
+
"""
|
|
251
|
+
items, total = get_embedded_page(resource, rel)
|
|
252
|
+
if items is None:
|
|
253
|
+
return False
|
|
254
|
+
return total is None or len(items) >= total
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_embedded(resource: Optional[Dict[str, Any]], path: str, default=None):
|
|
258
|
+
"""
|
|
259
|
+
Follow an embed path through `_embedded`.
|
|
260
|
+
|
|
261
|
+
Returns the embedded object for single links (owningCollection), a list
|
|
262
|
+
for list links (bundles), and a flat list when a list appears anywhere on
|
|
263
|
+
the path (bundles/bitstreams -> all bitstreams of all bundles).
|
|
264
|
+
`default` is returned when the first step was not embedded.
|
|
265
|
+
|
|
266
|
+
Parameters:
|
|
267
|
+
resource (Optional[Dict[str, Any]]): The resource to traverse.
|
|
268
|
+
path (str): The embed path to follow through `_embedded`.
|
|
269
|
+
default (Any, optional): The value to return if the first step is not embedded.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The embedded object, a list of embedded items, or the default value if not found.
|
|
273
|
+
"""
|
|
274
|
+
nodes: List[Any] = [resource]
|
|
275
|
+
is_list = False
|
|
276
|
+
for step, rel in enumerate(_clean_path(path).split("/")):
|
|
277
|
+
found = []
|
|
278
|
+
for node in nodes:
|
|
279
|
+
value = ((node or {}).get("_embedded") or {}).get(rel)
|
|
280
|
+
if value is None:
|
|
281
|
+
continue
|
|
282
|
+
if isinstance(value, list) or _is_page(value):
|
|
283
|
+
is_list = True
|
|
284
|
+
found.extend(value if isinstance(value, list) else _page_items(value))
|
|
285
|
+
else:
|
|
286
|
+
found.append(value)
|
|
287
|
+
if step == 0 and not found and not is_list:
|
|
288
|
+
return default
|
|
289
|
+
nodes = found
|
|
290
|
+
if is_list:
|
|
291
|
+
return nodes
|
|
292
|
+
return nodes[0] if nodes else default
|