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.
@@ -0,0 +1,363 @@
1
+ # -*- coding: UTF-8 -*-
2
+ """
3
+ Community management for DSpace API (DSpace 7+ / DSpace-CRIS 7+)
4
+
5
+ Covered REST contract
6
+ GET /core/communities/{uuid}
7
+ GET /core/communities/search/top
8
+ GET /core/communities/{uuid}/subcommunities
9
+ GET /core/communities/{uuid}/collections
10
+ POST /core/communities top-level community
11
+ POST /core/communities?parent={uuid} subcommunity
12
+ PATCH /core/communities/{uuid} metadata
13
+ DELETE /core/communities/{uuid} (deletes everything below it!)
14
+ GET|POST|DELETE /core/communities/{uuid}/adminGroup
15
+ GET /discover/search/objects?configuration=editCommunity&dsoType=COMMUNITY
16
+ GET /core/communities/search/findAdminAuthorized|findEditAuthorized|findAddAuthorized
17
+ GET|POST /core/communities/{uuid}/logo, DELETE via the logo bitstream
18
+ GET /core/communities/{uuid}/parentCommunity
19
+
20
+ Shared with collections (see _containers.py): get/set/delete_logo,
21
+ get_parent_community, get_ancestors, patch_metadata, find_admin_authorized,
22
+ find_edit_authorized. iter_tree() walks a whole community hierarchy.
23
+ """
24
+
25
+ import logging
26
+ from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
27
+
28
+ from ._base import BaseManager
29
+ from ._containers import ContainerMixin
30
+ from .projection import EmbedArg, ProjectionArg
31
+ from .config import Endpoints
32
+
33
+ # Logger for the module
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # API endpoint for communities
37
+ COMMUNITIES = getattr(Endpoints, "COMMUNITIES", "/core/communities")
38
+
39
+ # Type alias for a reference to a DSpace object (UUID or full object)
40
+ Ref = Union[str, Dict[str, Any]]
41
+
42
+
43
+ def build_dso_metadata(title: str, description: Optional[str] = None, abstract: Optional[str] = None,
44
+ rights: Optional[str] = None, table_of_contents: Optional[str] = None,
45
+ language: Optional[str] = None, **extra: Any) -> Dict[str, List[Dict[str, Any]]]:
46
+ """
47
+ Metadata for a community/collection form. Empty or whitespace-only
48
+ values are left out. Fields: dc.title, dc.description (HTML allowed),
49
+ dc.description.abstract, dc.rights (HTML allowed),
50
+ dc.description.tableofcontents; extra={"dc.rights.license": "..."}.
51
+ Booleans become "true"/"false".
52
+
53
+ Parameters:
54
+ title (str): Title of the community/collection
55
+ description (Optional[str]): Description (HTML allowed)
56
+ abstract (Optional[str]): Abstract
57
+ rights (Optional[str]): Rights (HTML allowed)
58
+ table_of_contents (Optional[str]): Table of contents
59
+ language (Optional[str]): Language code for the metadata values
60
+ **extra: Additional metadata fields
61
+
62
+ Returns:
63
+ Dict[str, List[Dict[str, Any]]]: Metadata dictionary suitable for DSpace API
64
+ """
65
+ if not (title or "").strip():
66
+ raise ValueError("title is required")
67
+ fields = {
68
+ "dc.title": title,
69
+ "dc.description": description,
70
+ "dc.description.abstract": abstract,
71
+ "dc.rights": rights,
72
+ "dc.description.tableofcontents": table_of_contents,
73
+ **extra,
74
+ }
75
+ md = {}
76
+ for field, value in fields.items():
77
+ if value is None:
78
+ continue
79
+ value = ("true" if value else "false") if isinstance(value, bool) else str(value).strip()
80
+ if value:
81
+ md[field] = [{"value": value, "language": language}]
82
+ return md
83
+
84
+
85
+ class CommunityManager(ContainerMixin, BaseManager):
86
+ """Manages communities, their administrator group, logo and hierarchy"""
87
+
88
+ # Container configuration for communities
89
+ _CONTAINER_PATH = COMMUNITIES
90
+ _CONTAINER_KEY = "communities"
91
+
92
+ def __init__(self, auth):
93
+ """Initialize the CommunityManager with authentication.
94
+
95
+ Parameters:
96
+ auth: Authentication object for API access
97
+
98
+ """
99
+ super().__init__(auth)
100
+ self._group_manager = None
101
+
102
+ @property
103
+ def groups(self):
104
+ """Access the GroupManager for community groups.
105
+
106
+ Returns:
107
+ GroupManager: Manager for community groups
108
+ """
109
+ if self._group_manager is None:
110
+ from .groups import GroupManager
111
+ self._group_manager = GroupManager(self.auth)
112
+ return self._group_manager
113
+
114
+ # ------------------------------------------------------------------
115
+ # Read
116
+ # ------------------------------------------------------------------
117
+
118
+ def get_community(self, community_uuid: str, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> Dict[str, Any]:
119
+ """Get a community by UUID.
120
+
121
+ Parameters:
122
+ community_uuid (str): UUID of the community
123
+ all_languages (Optional[bool]): Whether to include all languages
124
+ projection (ProjectionArg): Projection for the response
125
+ embed (EmbedArg): Embedded resources to include
126
+
127
+ Returns:
128
+ Dict[str, Any]: Community details
129
+ """
130
+ return self._get_json(self._url(COMMUNITIES, f"/{community_uuid}"), f"Get community {community_uuid}",
131
+ all_languages=all_languages, projection=projection, embed=embed)
132
+
133
+ def get_top_communities(self, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
134
+ """Get the top-level communities.
135
+
136
+ Parameters:
137
+ all_languages (Optional[bool]): Whether to include all languages
138
+ projection (ProjectionArg): Projection for the response
139
+ embed (EmbedArg): Embedded resources to include
140
+
141
+ Returns:
142
+ List[Dict[str, Any]]: List of top-level communities
143
+ """
144
+ return list(self._iter_embedded(self._url(COMMUNITIES, "/search/top"), "communities", "Get top communities",
145
+ all_languages=all_languages, projection=projection, embed=embed))
146
+
147
+ def get_subcommunities(self, community_uuid: str, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
148
+ """Get subcommunities of a community.
149
+
150
+ Parameters:
151
+ community_uuid (str): UUID of the community
152
+ all_languages (Optional[bool]): Whether to include all languages
153
+ projection (ProjectionArg): Projection for the response
154
+ embed (EmbedArg): Embedded resources to include
155
+
156
+ Returns:
157
+ List[Dict[str, Any]]: List of subcommunities
158
+ """
159
+ url = self._url(COMMUNITIES, f"/{community_uuid}/subcommunities")
160
+ return list(self._iter_embedded(url, ("subcommunities", "communities"),
161
+ f"Get subcommunities of {community_uuid}", all_languages=all_languages, projection=projection, embed=embed))
162
+
163
+ def get_community_collections(self, community_uuid: str, all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
164
+ """Get collections of a community.
165
+
166
+ Parameters:
167
+ community_uuid (str): UUID of the community
168
+ all_languages (Optional[bool]): Whether to include all languages
169
+ projection (ProjectionArg): Projection for the response
170
+ embed (EmbedArg): Embedded resources to include
171
+
172
+ Returns:
173
+ List[Dict[str, Any]]: List of collections
174
+ """
175
+ url = self._url(COMMUNITIES, f"/{community_uuid}/collections")
176
+ return list(self._iter_embedded(url, ("collections", "mappedCollections"),
177
+ f"Get collections of {community_uuid}", all_languages=all_languages, projection=projection, embed=embed))
178
+
179
+ def search_editable_communities(self, query: str = "", configuration: str = "editCommunity",
180
+ max_results: Optional[int] = 20,
181
+ all_languages: Optional[bool] = None, projection: ProjectionArg = None, embed: EmbedArg = None) -> List[Dict[str, Any]]:
182
+ """Communities (top-level or sub) the current user can edit, e.g. to pick a parent.
183
+
184
+ Parameters:
185
+ query (str): Search query
186
+ configuration (str): Search configuration
187
+ max_results (Optional[int]): Maximum number of results
188
+ all_languages (Optional[bool]): Whether to include all languages
189
+ projection (ProjectionArg): Projection for the response
190
+ embed (EmbedArg): Embedded resources to include
191
+
192
+ Returns:
193
+ List[Dict[str, Any]]: List of editable communities
194
+ """
195
+ return self._discover(query, dso_type="COMMUNITY", configuration=configuration, sort="dc.title,ASC",
196
+ max_results=max_results, all_languages=all_languages, projection=projection, embed=embed)
197
+
198
+ def find_add_authorized(self, query: Optional[str] = None, max_results: Optional[int] = None,
199
+ embed: EmbedArg = None) -> List[Dict[str, Any]]:
200
+ """Communities in which the current user may create subcommunities or collections.
201
+
202
+ Parameters:
203
+ query (Optional[str]): Search query
204
+ max_results (Optional[int]): Maximum number of results
205
+ embed (EmbedArg): Embedded resources to include
206
+
207
+ Returns:
208
+ List[Dict[str, Any]]: List of communities where the user can add subcommunities or collections
209
+ """
210
+ return self._authorized_search("findAddAuthorized", query, max_results=max_results, embed=embed)
211
+
212
+ def iter_tree(self, community_uuid: Optional[str] = None, max_depth: Optional[int] = None,
213
+ include_collections: bool = True) -> Iterator[Tuple[int, str, Dict[str, Any]]]:
214
+ """Iterate over the community tree in a depth-first manner.
215
+
216
+ Parameters:
217
+ community_uuid (Optional[str]): UUID of the starting community. If None, starts at top-level communities.
218
+ max_depth (Optional[int]): Maximum depth to traverse
219
+ include_collections (bool): Whether to include collections in the iteration
220
+
221
+ Yields:
222
+ Tuple[int, str, Dict[str, Any]]: (depth, "community"|"collection", object)
223
+ """
224
+ roots = [self.get_community(community_uuid)] if community_uuid else self.get_top_communities()
225
+ stack = [(0, root) for root in reversed(roots)]
226
+ while stack:
227
+ depth, community = stack.pop()
228
+ yield depth, "community", community
229
+ if max_depth is not None and depth >= max_depth:
230
+ continue
231
+ if include_collections:
232
+ for collection in self.get_community_collections(community["uuid"]):
233
+ yield depth + 1, "collection", collection
234
+ for sub in reversed(self.get_subcommunities(community["uuid"])):
235
+ stack.append((depth + 1, sub))
236
+
237
+ # ------------------------------------------------------------------
238
+ # Write
239
+ # ------------------------------------------------------------------
240
+
241
+ def create_community(self, metadata: Dict[str, Any], parent_uuid: Optional[str] = None) -> Dict[str, Any]:
242
+ """
243
+ Create a community; with parent_uuid it becomes a subcommunity.
244
+ metadata: e.g. build_dso_metadata("Faculty of Music", description="...")
245
+
246
+ Parameters:
247
+ metadata (Dict[str, Any]): Metadata for the community
248
+ parent_uuid (Optional[str]): UUID of the parent community, if creating a subcommunity
249
+
250
+ Returns:
251
+ Dict[str, Any]: The created community object
252
+ """
253
+ md = self._format_metadata(metadata)
254
+ if "dc.title" not in md:
255
+ raise ValueError("metadata must contain dc.title")
256
+ body = {"name": md["dc.title"][0]["value"], "metadata": md, "type": "community"}
257
+ params = {"parent": parent_uuid} if parent_uuid else None
258
+ community = self._request("POST", self._url(COMMUNITIES), "Create community", expected=(200, 201),
259
+ headers=self._headers(), params=params, json=body).json()
260
+ logger.info("Created %s %s", "subcommunity" if parent_uuid else "community", community.get("uuid"))
261
+ return community
262
+
263
+ def update_community_metadata(self, community_uuid: str, operation: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
264
+ """
265
+ Update metadata of a community.
266
+
267
+ Parameters:
268
+ community_uuid (str): UUID of the community to update
269
+ operation (str): Patch operation (e.g., "add", "replace", "remove")
270
+ metadata (Dict[str, Any]): Metadata to apply with the operation
271
+
272
+ Returns:
273
+ Dict[str, Any]: The updated community object
274
+ """
275
+ url = self._url(COMMUNITIES, f"/{community_uuid}")
276
+ return self._request("PATCH", url, f"Update community {community_uuid}", headers=self._headers(),
277
+ json=self._metadata_patch_ops(operation, metadata)).json()
278
+
279
+ def delete_community(self, community_uuid: str) -> bool:
280
+ """
281
+ Delete a community.
282
+
283
+ Parameters:
284
+ community_uuid (str): UUID of the community to delete
285
+
286
+ Returns:
287
+ bool: True if the community was successfully deleted
288
+
289
+ WARNING: deletes all subcommunities, collections and items below it.
290
+ """
291
+ self._request("DELETE", self._url(COMMUNITIES, f"/{community_uuid}"),
292
+ f"Delete community {community_uuid}", expected=(204,))
293
+ logger.info("Deleted community %s", community_uuid)
294
+ return True
295
+
296
+ # ------------------------------------------------------------------
297
+ # Administrators
298
+ # ------------------------------------------------------------------
299
+
300
+ def get_admin_group(self, community_uuid: str) -> Optional[Dict[str, Any]]:
301
+ """Get the admin group for a community.
302
+
303
+ Parameters:
304
+ community_uuid (str): UUID of the community
305
+
306
+ Returns:
307
+ Optional[Dict[str, Any]]: The admin group object if it exists, None otherwise.
308
+ """
309
+ return self.groups.get_role_group("community", community_uuid, "adminGroup")
310
+
311
+ def get_or_create_admin_group(self, community_uuid: str, description: Optional[str] = None) -> Dict[str, Any]:
312
+ """Get or create the admin group for a community.
313
+
314
+ Parameters:
315
+ community_uuid (str): UUID of the community
316
+ description (Optional[str]): Description for the admin group if it needs to be created
317
+
318
+ Returns:
319
+ Dict[str, Any]: The admin group object
320
+ """
321
+ return self.groups.get_or_create_role_group("community", community_uuid, "adminGroup", description)
322
+
323
+ def delete_admin_group(self, community_uuid: str) -> bool:
324
+ """Delete the admin group for a community.
325
+
326
+ Parameters:
327
+ community_uuid (str): UUID of the community
328
+
329
+ Returns:
330
+ bool: True if the admin group was successfully deleted
331
+ """
332
+ return self.groups.delete_role_group("community", community_uuid, "adminGroup")
333
+
334
+ def add_admins(self, community_uuid: str, epersons: Iterable[Ref] = (), subgroups: Iterable[Ref] = (),
335
+ description: Optional[str] = None) -> Dict[str, Any]:
336
+ """Create the admin group if needed and add epersons and/or groups to it.
337
+
338
+ Parameters:
339
+ community_uuid (str): UUID of the community
340
+ epersons (Iterable[Ref]): Epersons to add to the admin group
341
+ subgroups (Iterable[Ref]): Subgroups to add to the admin group
342
+ description (Optional[str]): Description for the admin group if it needs to be created
343
+
344
+ Returns:
345
+ Dict[str, Any]: The admin group object
346
+ """
347
+ return self.groups.ensure_role_members("community", community_uuid, "adminGroup",
348
+ epersons, subgroups, description)
349
+
350
+ def get_admins(self, community_uuid: str) -> Dict[str, List[Dict[str, Any]]]:
351
+ """Get the members of the admin group for a community.
352
+
353
+ Parameters:
354
+ community_uuid (str): UUID of the community
355
+
356
+ Returns:
357
+ Dict[str, List[Dict[str, Any]]]: {"epersons": [...], "subgroups": [...]} of the admin group (empty if none).
358
+ """
359
+ group = self.get_admin_group(community_uuid)
360
+ if not group:
361
+ return {"epersons": [], "subgroups": []}
362
+ return {"epersons": self.groups.get_group_epersons(group["uuid"]),
363
+ "subgroups": self.groups.get_subgroups(group["uuid"])}
dspace_api/config.py ADDED
@@ -0,0 +1,254 @@
1
+ # -*- coding: UTF-8 -*-
2
+ """
3
+ Configuration and constants for DSpace API client
4
+ """
5
+
6
+ import os
7
+ from contextlib import contextmanager
8
+ from contextvars import ContextVar
9
+ from typing import Optional
10
+
11
+ from dotenv import find_dotenv, load_dotenv
12
+
13
+ from .projection import ALL_LANGUAGES, apply_view
14
+
15
+ # Projection that makes DSpace return metadata values in every language,
16
+ # instead of only the values matching the request locale.
17
+ ALL_LANGUAGES_PROJECTION = ALL_LANGUAGES
18
+
19
+ # Scoped override set by DSpaceConfig.languages(); None = use the config default.
20
+ # A ContextVar keeps the override local to the current thread / asyncio task.
21
+ _all_languages_scope: ContextVar[Optional[bool]] = ContextVar("dspace_all_languages", default=None)
22
+
23
+ # Environment variable truthy values
24
+ _TRUE_VALUES = ("1", "true", "yes", "on")
25
+
26
+
27
+ def _env_bool(name, default=False):
28
+ """Parse an environment variable as a boolean."""
29
+ value = os.getenv(name)
30
+ if value is None or not value.strip():
31
+ return default
32
+ return value.strip().lower() in _TRUE_VALUES
33
+
34
+
35
+ def _env_int(name, default):
36
+ """Parse an environment variable as an integer."""
37
+ value = os.getenv(name)
38
+ try:
39
+ return int(value) if value is not None and value.strip() else default
40
+ except ValueError:
41
+ return default
42
+
43
+
44
+ def _env_float(name, default):
45
+ """Parse an environment variable as a float."""
46
+ value = os.getenv(name)
47
+ try:
48
+ return float(value) if value is not None and value.strip() else default
49
+ except ValueError:
50
+ return default
51
+
52
+
53
+ def _env_int_set(name, default):
54
+ """Parse an environment variable as a set of integers."""
55
+ value = os.getenv(name)
56
+ if value is None or not value.strip():
57
+ return frozenset(default)
58
+ try:
59
+ return frozenset(int(v) for v in value.replace(";", ",").split(",") if v.strip())
60
+ except ValueError:
61
+ return frozenset(default)
62
+
63
+
64
+ # Statuses that usually mean "try again later" rather than "your request is wrong"
65
+ DEFAULT_RETRY_STATUSES = (429, 502, 503, 504)
66
+
67
+
68
+ def with_all_languages(params, enabled):
69
+ """
70
+ Return `params` with the allLanguages projection applied if `enabled` is True.
71
+
72
+ Accepts None, a dict or a list of (key, value) tuples and returns the
73
+ same shape (None becomes a list). An existing projection (e.g.
74
+ preventMetadataSecurity) is kept: DSpace accepts the parameter
75
+ several times and combines the projections.
76
+ """
77
+ return apply_view(params, ALL_LANGUAGES_PROJECTION) if enabled else params
78
+
79
+
80
+ class DSpaceConfig:
81
+ """
82
+ Configuration class for DSpace API settings
83
+ """
84
+
85
+ def __init__(self, dspace_url=None, version=8, all_languages=None, timeout=None,
86
+ max_retries=None, retry_backoff=None, retry_max_wait=None, retry_statuses=None,
87
+ token_refresh_margin=None, reauthenticate=None, env_file=True,
88
+ accept_user_agreement=None):
89
+ """
90
+ Robustness settings (each falls back to an environment variable / .env):
91
+
92
+ timeout DSPACE_TIMEOUT seconds per request (30)
93
+ max_retries DSPACE_MAX_RETRIES retries for transient failures (3)
94
+ retry_backoff DSPACE_RETRY_BACKOFF base seconds, doubled per attempt (1.0)
95
+ retry_max_wait DSPACE_RETRY_MAX_WAIT cap for one wait, incl. Retry-After (30)
96
+ retry_statuses DSPACE_RETRY_STATUSES e.g. "429,502,503,504"
97
+ token_refresh_margin DSPACE_TOKEN_REFRESH_MARGIN refresh the login token this many
98
+ seconds before it expires (60)
99
+ reauthenticate DSPACE_REAUTHENTICATE log in again on 401 / expiry (true)
100
+ accept_user_agreement DSPACE_ACCEPT_USER_AGREEMENT DSpace-CRIS: accept the end-user
101
+ agreement for the logged-in user when
102
+ the server demands it (false)
103
+
104
+ env_file: True (default) reads ./.env, a path reads that file, False
105
+ reads none. Variables already set in the environment always win over
106
+ the file, so a stray .env cannot override CI or deployment settings.
107
+ """
108
+ if env_file:
109
+ # find_dotenv(usecwd=True): search from the working directory, not from site-packages
110
+ path = find_dotenv(usecwd=True) if env_file is True else os.fspath(env_file)
111
+ load_dotenv(dotenv_path=path or None, override=False)
112
+
113
+ self.dspace_url = dspace_url or os.getenv("DSPACE_URL", "http://localhost:8080/server/api")
114
+ self.version = version
115
+
116
+ # Default for read requests: ask for metadata in all languages?
117
+ # None -> DSPACE_ALL_LANGUAGES from the environment / .env (default: off)
118
+ if all_languages is None:
119
+ all_languages = _env_bool("DSPACE_ALL_LANGUAGES", False)
120
+ self.all_languages = bool(all_languages)
121
+
122
+ def pick(value, env_value):
123
+ """Pick the value to use: prefer the explicit value, fallback to the environment value."""
124
+ return env_value if value is None else value
125
+
126
+ self.timeout = pick(timeout, _env_float("DSPACE_TIMEOUT", 30))
127
+ self.max_retries = max(0, int(pick(max_retries, _env_int("DSPACE_MAX_RETRIES", 3))))
128
+ self.retry_backoff = max(0.0, float(pick(retry_backoff, _env_float("DSPACE_RETRY_BACKOFF", 1.0))))
129
+ self.retry_max_wait = max(0.0, float(pick(retry_max_wait, _env_float("DSPACE_RETRY_MAX_WAIT", 30))))
130
+ self.retry_statuses = frozenset(retry_statuses) if retry_statuses is not None else \
131
+ _env_int_set("DSPACE_RETRY_STATUSES", DEFAULT_RETRY_STATUSES)
132
+ self.token_refresh_margin = max(0.0, float(pick(token_refresh_margin,
133
+ _env_float("DSPACE_TOKEN_REFRESH_MARGIN", 60))))
134
+ self.reauthenticate = bool(pick(reauthenticate, _env_bool("DSPACE_REAUTHENTICATE", True)))
135
+ self.accept_user_agreement = bool(pick(accept_user_agreement,
136
+ _env_bool("DSPACE_ACCEPT_USER_AGREEMENT", False)))
137
+
138
+ # Remove trailing slash if present
139
+ if self.dspace_url.endswith('/'):
140
+ self.dspace_url = self.dspace_url[:-1]
141
+
142
+ @property
143
+ def headers(self):
144
+ """Default headers for API requests.
145
+
146
+ Returns:
147
+ Dict[str, str]: The default headers for API requests.
148
+ """
149
+ return {
150
+ "Content-Type": "application/json",
151
+ "Accept": "application/json",
152
+ }
153
+
154
+ def use_all_languages(self, call_value=None):
155
+ """
156
+ Whether a read request should use projection=allLanguages.
157
+ Precedence: explicit per-call value > languages() scope > config default.
158
+
159
+ Parameters:
160
+ call_value (Optional[bool]): Explicit per-call value for allLanguages. If None, the scope or config default is used.
161
+
162
+ Returns:
163
+ bool: True if allLanguages should be used for the request, False otherwise.
164
+ """
165
+ if call_value is not None:
166
+ return bool(call_value)
167
+ scoped = _all_languages_scope.get()
168
+ return self.all_languages if scoped is None else scoped
169
+
170
+ @contextmanager
171
+ def languages(self, all_languages=True):
172
+ """
173
+ Temporarily switch allLanguages on/off for every call in the block:
174
+
175
+ with client.config.languages(all_languages=True):
176
+ item = client.items.get_item(uuid)
177
+
178
+ The scope applies to all clients in the current thread/task;
179
+ an explicit all_languages=... argument on a call still wins.
180
+
181
+ Parameters:
182
+ all_languages (bool): Whether to enable allLanguages for the scope of the block.
183
+ """
184
+ token = _all_languages_scope.set(bool(all_languages))
185
+ try:
186
+ yield self
187
+ finally:
188
+ _all_languages_scope.reset(token)
189
+
190
+
191
+ # API Endpoints
192
+ class Endpoints:
193
+ """API endpoint constants"""
194
+
195
+ # Authentication
196
+ AUTHN_STATUS = "/authn/status"
197
+ AUTHN_LOGIN = "/authn/login"
198
+ AUTHN_LOGOUT = "/authn/logout"
199
+ CSRF = "/security/csrf" # only DSpace 8+
200
+
201
+ # Core API
202
+ ITEMS = "/core/items"
203
+ COMMUNITIES = "/core/communities"
204
+ COLLECTIONS = "/core/collections"
205
+ BUNDLES = "/core/bundles"
206
+ BITSTREAMS = "/core/bitstreams"
207
+ BITSTREAM_FORMATS = "/core/bitstreamformats"
208
+ SITES = "/core/sites"
209
+ ENTITY_TYPES = "/core/entitytypes"
210
+
211
+ # Authorization / access control
212
+ RESOURCE_POLICIES = "/authz/resourcepolicies"
213
+ GROUPS = "/eperson/groups"
214
+ EPERSONS = "/eperson/epersons"
215
+ REGISTRATIONS = "/eperson/registrations"
216
+ BULK_ACCESS_CONDITION_OPTIONS = "/config/bulkaccessconditionoptions"
217
+ SCRIPTS = "/system/scripts"
218
+ PROCESSES = "/system/processes"
219
+
220
+ # Submission
221
+ WORKSPACE_ITEMS = "/submission/workspaceitems"
222
+ WORKFLOW_ITEMS = "/workflow/workflowitems"
223
+
224
+ # Discovery
225
+ DISCOVER_SEARCH = "/discover/search/objects"
226
+
227
+ # Vocabulary
228
+ VOCABULARIES = "/submission/vocabularies"
229
+ VOCABULARY_ENTRIES = "/submission/vocabularyEntryDetails"
230
+
231
+
232
+ # Entity Types
233
+ class EntityTypes:
234
+ """DSpace entity type constants"""
235
+ PUBLICATION = "Publication"
236
+ PERSON = "Person"
237
+ PROJECT = "Project"
238
+ ORGUNIT = "OrgUnit"
239
+ JOURNAL = "Journal"
240
+ JOURNALVOLUME = "JournalVolume"
241
+ JOURNALISSUE = "JournalIssue"
242
+
243
+
244
+ # COAR Types
245
+ class CoarTypes:
246
+ """Common COAR publication type constants"""
247
+ STILL_IMAGE = "publication-coar-types:c_ecc8"
248
+ ARTICLE = "publication-coar-types:c_6501"
249
+ BOOK = "publication-coar-types:c_2f33"
250
+ THESIS = "publication-coar-types:c_46ec"
251
+
252
+
253
+ # Common constants
254
+ DEFAULT_COLLECTION_UUID = "eead452e-0238-4e7e-912f-70cecd4a1f67"