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/epersons.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
EPerson (user account) management for DSpace API (DSpace 7+ / DSpace-CRIS 7+)
|
|
4
|
+
|
|
5
|
+
EPersons
|
|
6
|
+
GET /eperson/epersons list (admin)
|
|
7
|
+
POST /eperson/epersons create (admin)
|
|
8
|
+
POST /eperson/epersons?token= create from a registration token
|
|
9
|
+
POST /eperson/epersons/{uuid}?token=&override= merge registration data
|
|
10
|
+
GET /eperson/epersons/{uuid}
|
|
11
|
+
PATCH /eperson/epersons/{uuid} metadata, /canLogin, /certificate,
|
|
12
|
+
/email, /netid, /password
|
|
13
|
+
DELETE /eperson/epersons/{uuid}
|
|
14
|
+
GET /eperson/epersons/{uuid}/groups direct group memberships
|
|
15
|
+
GET /eperson/epersons/search/byEmail?email=
|
|
16
|
+
GET /eperson/epersons/search/byMetadata?query=
|
|
17
|
+
GET /eperson/epersons/search/isNotMemberOf?group=&query=
|
|
18
|
+
|
|
19
|
+
Current user
|
|
20
|
+
GET /authn/status?embed=eperson
|
|
21
|
+
|
|
22
|
+
Registrations (self-registration / forgot password)
|
|
23
|
+
POST /eperson/registrations?accountRequestType=register|forgot
|
|
24
|
+
GET /eperson/registrations/search/findByToken?token=
|
|
25
|
+
PATCH /eperson/registrations/{id}?token=
|
|
26
|
+
|
|
27
|
+
End-user agreement (terms of use acceptance)
|
|
28
|
+
stored as eperson metadata "dspace.agreements.end-user" = "true"
|
|
29
|
+
(the terms text itself lives on the Site object, see site.py)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
34
|
+
|
|
35
|
+
from ._base import BaseManager
|
|
36
|
+
from .projection import EmbedArg, ProjectionArg
|
|
37
|
+
from .config import Endpoints
|
|
38
|
+
from .exceptions import NotFoundError
|
|
39
|
+
|
|
40
|
+
# Logger for the module
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# Constants for epersons, registrations, and authentication endpoints
|
|
44
|
+
EPERSONS = getattr(Endpoints, "EPERSONS", "/eperson/epersons")
|
|
45
|
+
REGISTRATIONS = getattr(Endpoints, "REGISTRATIONS", "/eperson/registrations")
|
|
46
|
+
AUTHN_STATUS = getattr(Endpoints, "AUTHN_STATUS", "/authn/status")
|
|
47
|
+
|
|
48
|
+
# Constants for end-user agreement metadata keys and request types
|
|
49
|
+
END_USER_AGREEMENT = "dspace.agreements.end-user"
|
|
50
|
+
# DSpace-CRIS: an eperson with this flag never has to accept the agreement (service accounts)
|
|
51
|
+
IGNORE_END_USER_AGREEMENT = "dspace.agreements.ignore"
|
|
52
|
+
REQUEST_TYPES = ("register", "forgot")
|
|
53
|
+
|
|
54
|
+
# Type alias for references to epersons (either UUID string or full dictionary)
|
|
55
|
+
Ref = Union[str, Dict[str, Any]]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _bool_str(value: bool) -> str:
|
|
59
|
+
"""Convert a boolean value to a string suitable for DSpace PATCH operations."""
|
|
60
|
+
# DSpace's PATCH operations for these flags take "true"/"false" strings
|
|
61
|
+
return "true" if value else "false"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class EPersonManager(BaseManager):
|
|
65
|
+
"""Manages DSpace epersons, the current user, registrations and agreements"""
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Lookup
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def get_eperson(self, eperson: Ref, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
72
|
+
"""Get an eperson by UUID or reference.
|
|
73
|
+
|
|
74
|
+
Parameters:
|
|
75
|
+
eperson (Ref): UUID string or full eperson dictionary.
|
|
76
|
+
all_languages (Optional[bool], optional): Whether to include all languages. Defaults to None.
|
|
77
|
+
projection (ProjectionArg, optional): Projection for the response. Defaults to None.
|
|
78
|
+
embed (EmbedArg, optional): Embedded resources for the response. Defaults to None.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict[str, Any]: The eperson dictionary.
|
|
82
|
+
"""
|
|
83
|
+
uuid = self.resolve_eperson(eperson)
|
|
84
|
+
return self._get_json(self._url(EPERSONS, f"/{uuid}"), f"Get eperson {uuid}", all_languages=all_languages, projection=projection, embed=embed)
|
|
85
|
+
|
|
86
|
+
def iter_epersons(self, page_size: Optional[int] = None, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None):
|
|
87
|
+
"""Every account in the repository (admin only). There may be a lot.
|
|
88
|
+
|
|
89
|
+
Parameters:
|
|
90
|
+
page_size (Optional[int], optional): Number of results per page. Defaults to None.
|
|
91
|
+
all_languages (Optional[bool], optional): Whether to include all languages. Defaults to None.
|
|
92
|
+
projection (ProjectionArg, optional): Projection for the response. Defaults to None.
|
|
93
|
+
embed (EmbedArg, optional): Embedded resources for the response. Defaults to None.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Iterator[Dict[str, Any]]: An iterator over eperson dictionaries.
|
|
97
|
+
"""
|
|
98
|
+
return self._iter_embedded(self._url(EPERSONS), "epersons", "List epersons", page_size=page_size,
|
|
99
|
+
all_languages=all_languages, projection=projection, embed=embed)
|
|
100
|
+
|
|
101
|
+
def find_by_email(self, email: str, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Optional[Dict[str, Any]]:
|
|
102
|
+
"""The eperson with this email, or None (204).
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
email (str): Email address of the eperson.
|
|
106
|
+
all_languages (Optional[bool], optional): Whether to include all languages. Defaults to None.
|
|
107
|
+
projection (ProjectionArg, optional): Projection for the response. Defaults to None.
|
|
108
|
+
embed (EmbedArg, optional): Embedded resources for the response. Defaults to None.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Optional[Dict[str, Any]]: The eperson dictionary if found, otherwise None.
|
|
112
|
+
"""
|
|
113
|
+
return self._get_optional(self._url(EPERSONS, "/search/byEmail"), "Find eperson by email",
|
|
114
|
+
params={"email": email}, all_languages=all_languages, projection=projection, embed=embed)
|
|
115
|
+
|
|
116
|
+
def search_epersons(self, query: str, max_results: Optional[int] = None,
|
|
117
|
+
all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
118
|
+
"""Search first name, last name, email and exact UUID (byMetadata).
|
|
119
|
+
|
|
120
|
+
Parameters:
|
|
121
|
+
query (str): Search query string.
|
|
122
|
+
max_results (Optional[int], optional): Maximum number of results to return. Defaults to None.
|
|
123
|
+
all_languages (Optional[bool], optional): Whether to include all languages. Defaults to None.
|
|
124
|
+
projection (ProjectionArg, optional): Projection for the response. Defaults to None.
|
|
125
|
+
embed (EmbedArg, optional): Embedded resources for the response. Defaults to None.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List[Dict[str, Any]]: A list of eperson dictionaries matching the search query.
|
|
129
|
+
"""
|
|
130
|
+
url = self._url(EPERSONS, "/search/byMetadata")
|
|
131
|
+
return list(self._iter_embedded(url, "epersons", f"Search epersons '{query}'", [("query", query)],
|
|
132
|
+
max_results=max_results, all_languages=all_languages, projection=projection, embed=embed))
|
|
133
|
+
|
|
134
|
+
def search_epersons_not_member_of(self, group_uuid: str, query: str = "",
|
|
135
|
+
max_results: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
136
|
+
"""Epersons that could be added to the group (isNotMemberOf).
|
|
137
|
+
|
|
138
|
+
Parameters:
|
|
139
|
+
group_uuid (str): UUID of the group.
|
|
140
|
+
query (str, optional): Search query string. Defaults to "".
|
|
141
|
+
max_results (Optional[int], optional): Maximum number of results to return. Defaults to None.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List[Dict[str, Any]]: A list of eperson dictionaries that are not members of the group.
|
|
145
|
+
"""
|
|
146
|
+
url = self._url(EPERSONS, "/search/isNotMemberOf")
|
|
147
|
+
return list(self._iter_embedded(url, "epersons", "Search addable epersons",
|
|
148
|
+
[("group", group_uuid), ("query", query)], max_results=max_results))
|
|
149
|
+
|
|
150
|
+
def resolve_eperson(self, eperson: Ref) -> str:
|
|
151
|
+
"""UUID for an eperson given as UUID, href, dict or email address.
|
|
152
|
+
|
|
153
|
+
Parameters:
|
|
154
|
+
eperson (Ref): Reference to the eperson (UUID, href, dict or email address).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str: UUID of the eperson.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
NotFoundError: If the eperson cannot be found by email.
|
|
161
|
+
ValueError: If the eperson reference cannot be resolved.
|
|
162
|
+
"""
|
|
163
|
+
uuid = self._uuid_of(eperson)
|
|
164
|
+
if uuid:
|
|
165
|
+
return uuid
|
|
166
|
+
if isinstance(eperson, str) and "@" in eperson:
|
|
167
|
+
found = self.find_by_email(eperson)
|
|
168
|
+
if not found:
|
|
169
|
+
raise NotFoundError(f"No eperson with email {eperson!r}")
|
|
170
|
+
return found["uuid"]
|
|
171
|
+
raise ValueError(f"Cannot resolve eperson reference {eperson!r} (use a UUID, href or email)")
|
|
172
|
+
|
|
173
|
+
def get_eperson_groups(self, eperson: Ref, embed_object: bool = True) -> List[Dict[str, Any]]:
|
|
174
|
+
"""Groups the eperson is a *direct* member of (linked DSO embedded by default).
|
|
175
|
+
|
|
176
|
+
Parameters:
|
|
177
|
+
eperson (Ref): Reference to the eperson (UUID, href, dict or email address).
|
|
178
|
+
embed_object (bool, optional): Whether to embed the linked DSO objects. Defaults to True.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List[Dict[str, Any]]: A list of groups the eperson is a direct member of.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
uuid = self.resolve_eperson(eperson)
|
|
185
|
+
params = [("embed", "object")] if embed_object else None
|
|
186
|
+
return list(self._iter_embedded(self._url(EPERSONS, f"/{uuid}/groups"), "groups",
|
|
187
|
+
f"Get groups of eperson {uuid}", params))
|
|
188
|
+
|
|
189
|
+
# ------------------------------------------------------------------
|
|
190
|
+
# Current user
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def get_auth_status(self, embed_eperson: bool = True, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
194
|
+
"""Get the authentication status of the current user.
|
|
195
|
+
|
|
196
|
+
/authn/status, e.g. {"authenticated": true, "authenticationMethod": "password", ...}
|
|
197
|
+
|
|
198
|
+
Parameters:
|
|
199
|
+
embed_eperson (bool, optional): Whether to embed the eperson object. Defaults to True.
|
|
200
|
+
all_languages (Optional[bool], optional): Whether to include all languages. Defaults to None.
|
|
201
|
+
projection (ProjectionArg, optional): Projection parameter. Defaults to None.
|
|
202
|
+
embed (EmbedArg, optional): Embed parameter. Defaults to None.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dict[str, Any]: Authentication status of the current user.
|
|
206
|
+
"""
|
|
207
|
+
params = [("embed", "eperson")] if embed_eperson else None
|
|
208
|
+
return self._get_json(self._url(AUTHN_STATUS), "Get authentication status", params,
|
|
209
|
+
all_languages=all_languages, projection=projection, embed=embed)
|
|
210
|
+
|
|
211
|
+
def get_current_user(self, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Optional[Dict[str, Any]]:
|
|
212
|
+
"""The logged-in eperson, or None when the session is anonymous.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
all_languages (Optional[bool], optional): Whether to include all languages. Defaults to None.
|
|
216
|
+
projection (ProjectionArg, optional): Projection parameter. Defaults to None.
|
|
217
|
+
embed (EmbedArg, optional): Embed parameter. Defaults to None.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Optional[Dict[str, Any]]: The logged-in eperson's data, or None if anonymous.
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
status = self.get_auth_status(all_languages=all_languages, projection=projection, embed=embed)
|
|
224
|
+
if not status.get("authenticated"):
|
|
225
|
+
return None
|
|
226
|
+
embedded = (status.get("_embedded") or {}).get("eperson")
|
|
227
|
+
if embedded:
|
|
228
|
+
return embedded
|
|
229
|
+
href = ((status.get("_links") or {}).get("eperson") or {}).get("href")
|
|
230
|
+
return self.get_by_href(href, all_languages=all_languages, projection=projection, embed=embed) if href else None
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
# Create / update / delete
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def _person_metadata(self, first_name, last_name, language, metadata) -> Dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
Prepare the metadata dictionary for an eperson.
|
|
239
|
+
|
|
240
|
+
Parameters:
|
|
241
|
+
first_name (str): First name of the eperson.
|
|
242
|
+
last_name (str): Last name of the eperson.
|
|
243
|
+
language (str): Language of the eperson.
|
|
244
|
+
metadata (Optional[Dict[str, Any]]): Existing metadata to include.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dict[str, Any]: Formatted metadata dictionary.
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
md = dict(metadata or {})
|
|
251
|
+
for field, value in (("eperson.firstname", first_name), ("eperson.lastname", last_name),
|
|
252
|
+
("eperson.language", language)):
|
|
253
|
+
if value and field not in md:
|
|
254
|
+
md[field] = value
|
|
255
|
+
return self._format_metadata(md)
|
|
256
|
+
|
|
257
|
+
def create_eperson(self, email: str, first_name: Optional[str] = None, last_name: Optional[str] = None,
|
|
258
|
+
can_log_in: bool = True, require_certificate: bool = False,
|
|
259
|
+
net_id: Optional[str] = None, language: Optional[str] = None,
|
|
260
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
261
|
+
"""
|
|
262
|
+
Create an account (admin only). 422 if the email is missing or taken.
|
|
263
|
+
The account has no password; set one with set_password() or let the
|
|
264
|
+
user use "forgot password" (request_registration(email, "forgot")).
|
|
265
|
+
|
|
266
|
+
Parameters:
|
|
267
|
+
email (str): Email address of the eperson.
|
|
268
|
+
first_name (Optional[str], optional): First name of the eperson. Defaults to None.
|
|
269
|
+
last_name (Optional[str], optional): Last name of the eperson. Defaults to None.
|
|
270
|
+
can_log_in (bool, optional): Whether the eperson can log in. Defaults to True.
|
|
271
|
+
require_certificate (bool, optional): Whether the eperson requires a certificate. Defaults to False.
|
|
272
|
+
net_id (Optional[str], optional): Net ID of the eperson. Defaults to None.
|
|
273
|
+
language (Optional[str], optional): Language of the eperson. Defaults to None.
|
|
274
|
+
metadata (Optional[Dict[str, Any]], optional): Additional metadata for the eperson. Defaults to None.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Dict[str, Any]: The created eperson's data.
|
|
278
|
+
"""
|
|
279
|
+
body = {
|
|
280
|
+
"name": email,
|
|
281
|
+
"email": email,
|
|
282
|
+
"canLogIn": can_log_in,
|
|
283
|
+
"requireCertificate": require_certificate,
|
|
284
|
+
"selfRegistered": False,
|
|
285
|
+
"metadata": self._person_metadata(first_name, last_name, language, metadata),
|
|
286
|
+
"type": "eperson",
|
|
287
|
+
}
|
|
288
|
+
if net_id:
|
|
289
|
+
body["netid"] = net_id
|
|
290
|
+
person = self._request("POST", self._url(EPERSONS), f"Create eperson {email}", expected=(200, 201),
|
|
291
|
+
headers=self._headers(), json=body).json()
|
|
292
|
+
logger.info("Created eperson %s (%s)", person.get("uuid"), email)
|
|
293
|
+
return person
|
|
294
|
+
|
|
295
|
+
def get_or_create_eperson(self, email: str, **kwargs) -> Dict[str, Any]:
|
|
296
|
+
"""
|
|
297
|
+
Get an eperson by email, or create one if it doesn't exist.
|
|
298
|
+
|
|
299
|
+
Parameters:
|
|
300
|
+
email (str): Email address of the eperson.
|
|
301
|
+
**kwargs: Additional arguments to pass to `create_eperson`.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dict[str, Any]: The eperson's data.
|
|
305
|
+
"""
|
|
306
|
+
return self.find_by_email(email) or self.create_eperson(email, **kwargs)
|
|
307
|
+
|
|
308
|
+
def create_eperson_from_token(self, token: str, first_name: str, last_name: str,
|
|
309
|
+
password: Optional[str] = None, email: Optional[str] = None,
|
|
310
|
+
language: Optional[str] = None,
|
|
311
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
312
|
+
"""
|
|
313
|
+
Finish a self-registration with the token from the registration email.
|
|
314
|
+
First and last name are mandatory; email (if given) must match the registration.
|
|
315
|
+
|
|
316
|
+
Parameters:
|
|
317
|
+
token (str): Registration token from the email.
|
|
318
|
+
first_name (str): First name of the eperson.
|
|
319
|
+
last_name (str): Last name of the eperson.
|
|
320
|
+
password (Optional[str], optional): Password for the eperson. Defaults to None.
|
|
321
|
+
email (Optional[str], optional): Email address of the eperson. Defaults to None.
|
|
322
|
+
language (Optional[str], optional): Language of the eperson. Defaults to None.
|
|
323
|
+
metadata (Optional[Dict[str, Any]], optional): Additional metadata for the eperson. Defaults to None.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Dict[str, Any]: The created eperson's data.
|
|
327
|
+
"""
|
|
328
|
+
body: Dict[str, Any] = {
|
|
329
|
+
"canLogIn": True,
|
|
330
|
+
"requireCertificate": False,
|
|
331
|
+
"metadata": self._person_metadata(first_name, last_name, language, metadata),
|
|
332
|
+
"type": "eperson",
|
|
333
|
+
}
|
|
334
|
+
if password:
|
|
335
|
+
body["password"] = password
|
|
336
|
+
if email:
|
|
337
|
+
body["email"] = email
|
|
338
|
+
return self._request("POST", self._url(EPERSONS), "Create eperson from registration token",
|
|
339
|
+
expected=(200, 201), headers=self._headers(), params={"token": token},
|
|
340
|
+
json=body).json()
|
|
341
|
+
|
|
342
|
+
def patch_eperson(self, eperson: Ref, operations: List[Dict[str, Any]],
|
|
343
|
+
params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
344
|
+
"""Patch an eperson with the given operations.
|
|
345
|
+
|
|
346
|
+
Parameters:
|
|
347
|
+
eperson (Ref): Reference to the eperson to patch.
|
|
348
|
+
operations (List[Dict[str, Any]]): List of patch operations.
|
|
349
|
+
params (Optional[Dict[str, Any]], optional): Additional query parameters. Defaults to None.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Dict[str, Any]: The updated eperson's data.
|
|
353
|
+
"""
|
|
354
|
+
uuid = self.resolve_eperson(eperson)
|
|
355
|
+
response = self._request("PATCH", self._url(EPERSONS, f"/{uuid}"), f"Patch eperson {uuid}",
|
|
356
|
+
headers=self._headers(), json=operations, params=params)
|
|
357
|
+
return response.json()
|
|
358
|
+
|
|
359
|
+
def update_eperson_metadata(self, eperson: Ref, operation: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
360
|
+
"""Update eperson metadata.
|
|
361
|
+
|
|
362
|
+
add / replace / remove eperson metadata (admins: any account; users: their own).
|
|
363
|
+
|
|
364
|
+
Parameters:
|
|
365
|
+
eperson (Ref): Reference to the eperson to update metadata for.
|
|
366
|
+
operation (str): The metadata operation to perform ("add", "replace", "remove").
|
|
367
|
+
metadata (Dict[str, Any]): The metadata to update.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Dict[str, Any]: The updated eperson's data.
|
|
371
|
+
"""
|
|
372
|
+
return self.patch_eperson(eperson, self._metadata_patch_ops(operation, metadata))
|
|
373
|
+
|
|
374
|
+
def upsert_eperson_metadata(self, eperson: Ref, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
375
|
+
"""replace fields the account already has, add the others.
|
|
376
|
+
|
|
377
|
+
Parameters:
|
|
378
|
+
eperson (Ref): Reference to the eperson to update metadata for.
|
|
379
|
+
metadata (Dict[str, Any]): The metadata to upsert.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Dict[str, Any]: The updated eperson's data.
|
|
383
|
+
"""
|
|
384
|
+
current = self.get_eperson(eperson, all_languages=True)
|
|
385
|
+
ops = self._upsert_ops(current.get("metadata") or {}, metadata, self._format_metadata_field)
|
|
386
|
+
return self.patch_eperson(current["uuid"], ops)
|
|
387
|
+
|
|
388
|
+
def set_can_log_in(self, eperson: Ref, can_log_in: bool) -> Dict[str, Any]:
|
|
389
|
+
"""Enable/disable the account (admin only).
|
|
390
|
+
|
|
391
|
+
Parameters:
|
|
392
|
+
eperson (Ref): Reference to the eperson to update.
|
|
393
|
+
can_log_in (bool): Whether the account can log in.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Dict[str, Any]: The updated eperson's data.
|
|
397
|
+
"""
|
|
398
|
+
return self.patch_eperson(eperson, [{"op": "replace", "path": "/canLogin", "value": _bool_str(can_log_in)}])
|
|
399
|
+
|
|
400
|
+
def set_require_certificate(self, eperson: Ref, required: bool) -> Dict[str, Any]:
|
|
401
|
+
"""Set whether the eperson requires a certificate (admin only).
|
|
402
|
+
|
|
403
|
+
Parameters:
|
|
404
|
+
eperson (Ref): Reference to the eperson to update.
|
|
405
|
+
required (bool): Whether a certificate is required.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Dict[str, Any]: The updated eperson's data.
|
|
409
|
+
"""
|
|
410
|
+
return self.patch_eperson(eperson, [{"op": "replace", "path": "/certificate", "value": _bool_str(required)}])
|
|
411
|
+
|
|
412
|
+
def set_email(self, eperson: Ref, email: str) -> Dict[str, Any]:
|
|
413
|
+
"""Set the email address of the eperson (admin only).
|
|
414
|
+
|
|
415
|
+
Parameters:
|
|
416
|
+
eperson (Ref): Reference to the eperson to update.
|
|
417
|
+
email (str): The email address to set.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Dict[str, Any]: The updated eperson's data.
|
|
421
|
+
"""
|
|
422
|
+
return self.patch_eperson(eperson, [{"op": "replace", "path": "/email", "value": email}])
|
|
423
|
+
|
|
424
|
+
def set_netid(self, eperson: Ref, net_id: str) -> Dict[str, Any]:
|
|
425
|
+
"""Set or replace the netid of the eperson (admin only).
|
|
426
|
+
|
|
427
|
+
Parameters:
|
|
428
|
+
eperson (Ref): Reference to the eperson to update.
|
|
429
|
+
net_id (str): The netid to set.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Dict[str, Any]: The updated eperson's data.
|
|
433
|
+
"""
|
|
434
|
+
return self.patch_eperson(eperson, [{"op": "replace", "path": "/netid", "value": net_id}])
|
|
435
|
+
|
|
436
|
+
def set_password(self, eperson: Ref, new_password: str, current_password: Optional[str] = None,
|
|
437
|
+
token: Optional[str] = None) -> Dict[str, Any]:
|
|
438
|
+
"""
|
|
439
|
+
Change a password. Users changing their own need current_password (403
|
|
440
|
+
if wrong); a password reset uses the registration token instead.
|
|
441
|
+
422 if the password doesn't match the configured rules.
|
|
442
|
+
"""
|
|
443
|
+
value = {"new_password": new_password}
|
|
444
|
+
if current_password is not None:
|
|
445
|
+
value["current_password"] = current_password
|
|
446
|
+
return self.patch_eperson(eperson, [{"op": "add", "path": "/password", "value": value}],
|
|
447
|
+
params={"token": token} if token else None)
|
|
448
|
+
|
|
449
|
+
def delete_eperson(self, eperson: Ref) -> bool:
|
|
450
|
+
uuid = self.resolve_eperson(eperson)
|
|
451
|
+
self._request("DELETE", self._url(EPERSONS, f"/{uuid}"), f"Delete eperson {uuid}", expected=(204,))
|
|
452
|
+
logger.info("Deleted eperson %s", uuid)
|
|
453
|
+
return True
|
|
454
|
+
|
|
455
|
+
# ------------------------------------------------------------------
|
|
456
|
+
# Registrations
|
|
457
|
+
# ------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
def request_registration(self, email: str, request_type: str = "register") -> bool:
|
|
460
|
+
"""
|
|
461
|
+
Send a registration ("register") or password reset ("forgot") email.
|
|
462
|
+
Works anonymously; DSpace answers the same either way, so it does not
|
|
463
|
+
reveal whether the address has an account.
|
|
464
|
+
|
|
465
|
+
Parameters:
|
|
466
|
+
email (str): The email address to send the registration or password reset email to.
|
|
467
|
+
request_type (str, optional): The type of request, either "register" or "forgot". Defaults to "register".
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
bool: True if the request was successfully sent.
|
|
471
|
+
"""
|
|
472
|
+
if request_type not in REQUEST_TYPES:
|
|
473
|
+
raise ValueError(f"request_type must be one of {REQUEST_TYPES}")
|
|
474
|
+
self._request("POST", self._url(REGISTRATIONS), f"Request {request_type} for {email}",
|
|
475
|
+
expected=(200, 201, 204), headers=self._headers(),
|
|
476
|
+
params={"accountRequestType": request_type}, json={"email": email})
|
|
477
|
+
logger.info("Requested %s email for %s", request_type, email)
|
|
478
|
+
return True
|
|
479
|
+
|
|
480
|
+
def find_registration_by_token(self, token: str) -> Dict[str, Any]:
|
|
481
|
+
"""Email, linked user and (for ORCID etc.) registration metadata for a token.
|
|
482
|
+
|
|
483
|
+
Parameters:
|
|
484
|
+
token (str): The registration token to search for.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Dict[str, Any]: The registration data associated with the token.
|
|
488
|
+
"""
|
|
489
|
+
return self._get_json(self._url(REGISTRATIONS, "/search/findByToken"), "Find registration by token",
|
|
490
|
+
params={"token": token})
|
|
491
|
+
|
|
492
|
+
def update_registration_email(self, registration_id: int, token: str, email: str,
|
|
493
|
+
operation: str = "replace") -> bool:
|
|
494
|
+
"""Change/add the email of a registration; DSpace renews the token and sends a new mail.
|
|
495
|
+
|
|
496
|
+
Parameters:
|
|
497
|
+
registration_id (int): The ID of the registration to update.
|
|
498
|
+
token (str): The registration token.
|
|
499
|
+
email (str): The new email address to set.
|
|
500
|
+
operation (str, optional): The operation to perform, either "add" or "replace". Defaults to "replace".
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
bool: True if the email was successfully updated.
|
|
504
|
+
"""
|
|
505
|
+
if operation not in ("add", "replace"):
|
|
506
|
+
raise ValueError("operation must be 'add' or 'replace'")
|
|
507
|
+
self._request("PATCH", self._url(REGISTRATIONS, f"/{registration_id}"), "Update registration email",
|
|
508
|
+
expected=(200, 204), headers=self._headers(), params={"token": token},
|
|
509
|
+
json=[{"op": operation, "path": "/email", "value": [email]}])
|
|
510
|
+
return True
|
|
511
|
+
|
|
512
|
+
def merge_registration(self, eperson: Ref, token: str, override: Iterable[str] = ()) -> Dict[str, Any]:
|
|
513
|
+
"""Merge registration data (e.g. from ORCID login) into an existing account.
|
|
514
|
+
|
|
515
|
+
Parameters:
|
|
516
|
+
eperson (Ref): Reference to the eperson to merge registration data into.
|
|
517
|
+
token (str): The registration token.
|
|
518
|
+
override (Iterable[str], optional): Fields to override. Defaults to an empty tuple.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Dict[str, Any]: The updated eperson's data.
|
|
522
|
+
"""
|
|
523
|
+
uuid = self.resolve_eperson(eperson)
|
|
524
|
+
params = {"token": token}
|
|
525
|
+
override = list(override)
|
|
526
|
+
if override:
|
|
527
|
+
params["override"] = ",".join(override)
|
|
528
|
+
response = self._request("POST", self._url(EPERSONS, f"/{uuid}"), f"Merge registration into {uuid}",
|
|
529
|
+
expected=(200, 201), headers=self._headers(), params=params)
|
|
530
|
+
return self._json_or_none(response) or {}
|
|
531
|
+
|
|
532
|
+
# ------------------------------------------------------------------
|
|
533
|
+
# End-user agreement
|
|
534
|
+
# ------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
def has_accepted_end_user_agreement(self, eperson: Ref) -> bool:
|
|
537
|
+
"""Check if the end-user agreement has been accepted.
|
|
538
|
+
|
|
539
|
+
Parameters:
|
|
540
|
+
eperson (Ref): Reference to the eperson to check.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
bool: True if the end-user agreement has been accepted.
|
|
544
|
+
"""
|
|
545
|
+
person = eperson if isinstance(eperson, dict) and "metadata" in eperson else self.get_eperson(eperson)
|
|
546
|
+
values = (person.get("metadata") or {}).get(END_USER_AGREEMENT) or []
|
|
547
|
+
return bool(values) and str(values[0].get("value")).lower() == "true"
|
|
548
|
+
|
|
549
|
+
def accept_end_user_agreement(self, eperson: Ref) -> Dict[str, Any]:
|
|
550
|
+
"""
|
|
551
|
+
Record acceptance. Uses 'replace' when the field already exists
|
|
552
|
+
(a blind 'add' would append a second value), and is a no-op if it's
|
|
553
|
+
already "true". For the logged-in user the login token is refreshed
|
|
554
|
+
too: DSpace-CRIS reads the acceptance from a token claim, so without
|
|
555
|
+
a new token it keeps answering 403 "No user agreement accepted".
|
|
556
|
+
|
|
557
|
+
Parameters:
|
|
558
|
+
eperson (Ref): Reference to the eperson to update.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
Dict[str, Any]: The updated eperson's data.
|
|
562
|
+
"""
|
|
563
|
+
return self._set_agreement_flag(eperson, END_USER_AGREEMENT, True)
|
|
564
|
+
|
|
565
|
+
def revoke_end_user_agreement(self, eperson: Ref) -> Dict[str, Any]:
|
|
566
|
+
"""Remove the acceptance, so the user is asked again at next login.
|
|
567
|
+
|
|
568
|
+
Parameters:
|
|
569
|
+
eperson (Ref): Reference to the eperson to update.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Dict[str, Any]: The updated eperson's data.
|
|
573
|
+
"""
|
|
574
|
+
return self._set_agreement_flag(eperson, END_USER_AGREEMENT, False)
|
|
575
|
+
|
|
576
|
+
def ignore_end_user_agreement(self, eperson: Ref, ignore: bool = True) -> Dict[str, Any]:
|
|
577
|
+
"""
|
|
578
|
+
DSpace-CRIS: exempt an account (e.g. an import or harvesting service
|
|
579
|
+
account) from the end-user agreement ("dspace.agreements.ignore").
|
|
580
|
+
|
|
581
|
+
Parameters:
|
|
582
|
+
eperson (Ref): Reference to the eperson to update.
|
|
583
|
+
ignore (bool, optional): Whether to ignore the end-user agreement. Defaults to True.
|
|
584
|
+
ignore=False removes the exemption.
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
Dict[str, Any]: The updated eperson's data.
|
|
588
|
+
"""
|
|
589
|
+
return self._set_agreement_flag(eperson, IGNORE_END_USER_AGREEMENT, ignore)
|
|
590
|
+
|
|
591
|
+
def is_ignoring_end_user_agreement(self, eperson: Ref) -> bool:
|
|
592
|
+
"""Check if the end-user agreement is being ignored for the given eperson.
|
|
593
|
+
|
|
594
|
+
Parameters:
|
|
595
|
+
eperson (Ref): Reference to the eperson to check.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
bool: True if the end-user agreement is being ignored.
|
|
599
|
+
"""
|
|
600
|
+
person = eperson if isinstance(eperson, dict) and "metadata" in eperson else self.get_eperson(eperson)
|
|
601
|
+
values = (person.get("metadata") or {}).get(IGNORE_END_USER_AGREEMENT) or []
|
|
602
|
+
return bool(values) and str(values[0].get("value")).lower() == "true"
|
|
603
|
+
|
|
604
|
+
def _set_agreement_flag(self, eperson: Ref, field: str, enabled: bool) -> Dict[str, Any]:
|
|
605
|
+
"""Set or unset an agreement flag for the given eperson.
|
|
606
|
+
|
|
607
|
+
Parameters:
|
|
608
|
+
eperson (Ref): Reference to the eperson to update.
|
|
609
|
+
field (str): The metadata field representing the agreement flag.
|
|
610
|
+
enabled (bool): Whether to enable (True) or disable (False) the flag.
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
Dict[str, Any]: The updated eperson's data.
|
|
614
|
+
"""
|
|
615
|
+
person = self.get_eperson(eperson, all_languages=True)
|
|
616
|
+
values = (person.get("metadata") or {}).get(field) or []
|
|
617
|
+
if enabled and values and str(values[0].get("value")).lower() == "true":
|
|
618
|
+
return person
|
|
619
|
+
if not enabled and not values:
|
|
620
|
+
return person
|
|
621
|
+
if enabled:
|
|
622
|
+
operation = {"op": "replace" if values else "add", "path": f"/metadata/{field}",
|
|
623
|
+
"value": [{"value": "true"}]}
|
|
624
|
+
else:
|
|
625
|
+
operation = {"op": "remove", "path": f"/metadata/{field}"}
|
|
626
|
+
updated = self.patch_eperson(person["uuid"], [operation])
|
|
627
|
+
self._refresh_token_if_current(person["uuid"])
|
|
628
|
+
return updated
|
|
629
|
+
|
|
630
|
+
def _refresh_token_if_current(self, uuid: str) -> bool:
|
|
631
|
+
"""New login token when `uuid` is the logged-in user (agreement flags are token claims).
|
|
632
|
+
|
|
633
|
+
Parameters:
|
|
634
|
+
uuid (str): The UUID of the eperson to check against the current logged-in user.
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
bool: True if the token was refreshed, False otherwise.
|
|
638
|
+
"""
|
|
639
|
+
refresh = getattr(self.auth, "refresh_token", None)
|
|
640
|
+
if not callable(refresh) or getattr(self.auth, "is_authenticated", False) is not True:
|
|
641
|
+
return False
|
|
642
|
+
status = self.get_auth_status(embed_eperson=False)
|
|
643
|
+
href = ((status.get("_links") or {}).get("eperson") or {}).get("href") or ""
|
|
644
|
+
if not href.rstrip("/").endswith(uuid):
|
|
645
|
+
return False
|
|
646
|
+
refresh()
|
|
647
|
+
return True
|