From 90bfdc73c813ffe141c5e452163b3295f399f5ba Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Wed, 9 Oct 2024 17:25:09 +0200 Subject: [PATCH 01/22] added missing funcs for res policies updating --- dspace_rest_client/client.py | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index c651bff..591b9fd 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -263,6 +263,35 @@ def api_put(self, url, params, json, retry=False): return r + def api_put_uri(self, url, params, uri_list, retry=False): + """ + Perform a PUT request. Refresh XSRF token if necessary. + PUTs are typically used to update objects. + @param url: DSpace REST API URL + @param params: Any parameters to include (eg ?parent=abbc-....) + @param retry: Has this method already been retried? Used if we need to refresh XSRF. + @return: Response from API + """ + r = self.session.put(url, params=params, data=uri_list, headers=self.list_request_headers) + self.update_token(r) + + if r.status_code == 403: + # 403 Forbidden + # If we had a CSRF failure, retry the request with the updated token + # After speaking in #dev it seems that these do need occasional refreshes but I suspect + # it's happening too often for me, so check for accidentally triggering it + logging.debug(r.text) + # Parse response + r_json = parse_json(r) + if 'message' in r_json and 'CSRF token' in r_json['message']: + if retry: + logging.warning(f'Too many retries updating token: {r.status_code}: {r.text}') + else: + logging.debug("Retrying request with updated CSRF token") + return self.api_put_uri(url, params=params, uri_list=uri_list, retry=True) + + return r + def api_delete(self, url, params, retry=False): """ Perform a DELETE request. Refresh XSRF token if necessary. @@ -1025,3 +1054,57 @@ def solr_query(self, query, filters=None, fields=None, start=0, rows=999999999): return self.solr.search(query, fq=filters, start=start, rows=rows, **{ 'fl': ','.join(fields) }) + + def get_items_from_collection(self, collection_id, page=0, size=1000): + """ + Get all items + @return: list of Item objects + """ + url = f'{self.API_ENDPOINT}/discover/search/objects?sort=dc.date.accessioned,DESC&page={page}&size={size}&scope={collection_id}&dsoType=ITEM&embed=thumbnail' + + items = list() + r = self.api_get(url) + r_json = parse_json(r) + if '_embedded' in r_json: + if 'searchResult' in r_json['_embedded']: + if '_embedded' in r_json['_embedded']['searchResult']: + for item_resource in r_json['_embedded']['searchResult']['_embedded']['objects']: + items.append(Item(item_resource['_embedded']['indexableObject'])) + + return items + + def get_bundle_by_name(self, name, item_uuid): + """ + Get a bundle by name for a specific item + @param name: Name of the bundle + @param item_uuid: UUID of the item + @return: Bundle object + """ + url = f'{self.API_ENDPOINT}/core/items/{item_uuid}/bundles' + r_json = self.fetch_resource(url, params=None) + if '_embedded' in r_json: + if 'bundles' in r_json['_embedded']: + for bundle in r_json['_embedded']['bundles']: + if bundle['name'] == name: + return Bundle(bundle) + return None + + def get_resource_policy(self, bundle_uuid): + """ + Get a resource policy for a specific bundle + """ + url = f'{self.API_ENDPOINT}/authz/resourcepolicies/search/resource?uuid={bundle_uuid}&embed=eperson&embed=group' + r = self.api_get(url) + r_json = parse_json(r) + if '_embedded' in r_json: + if 'resourcepolicies' in r_json['_embedded']: + return r_json['_embedded']['resourcepolicies'][0] + + def update_resource_policy_group(self, policy_id, group_uuid): + """ + Update a resource policy with a new group + """ + url = f'{self.API_ENDPOINT}/authz/resourcepolicies/{policy_id}/group' + body = f'{self.API_ENDPOINT}/eperson/groups/{group_uuid}' + r = self.api_put_uri(url, None, body, False) + return r From fd49352f376aad0d143b86127359e7d0053d403f Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Tue, 22 Oct 2024 15:26:05 +0200 Subject: [PATCH 02/22] added License and Label models --- dspace_rest_client/models.py | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index de463ac..6a4679d 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -517,4 +517,100 @@ class RelationshipType(AddressableHALResource): def __init__(self, api_resource): super(RelationshipType, self).__init__(api_resource) + def get_metadata_values(self, field): + """ + Return metadata values as simple list of strings + @param field: DSpace field, eg. dc.creator + @return: list of strings + """ + values = list() + if field in self.metadata: + values = self.metadata[field] + return values + + def as_dict(self): + """ + Return a dict representation of this Item, based on super with item-specific attributes added + @return: dict of Item for API use + """ + dso_dict = super(Item, self).as_dict() + item_dict = {'inArchive': self.inArchive, 'discoverable': self.discoverable, 'withdrawn': self.withdrawn} + return {**dso_dict, **item_dict} + @classmethod + def from_dso(cls, dso: DSpaceObject): + # Create new Item and copy everything over from this dso + item = cls() + for key, value in dso.__dict__.items(): + item.__dict__[key] = value + return item + +class License(AddressableHALResource): + """ + Specific attributes and functions for licenses + """ + type = 'clarinlicense' + name = None + definition = None + confirmation = 0 + requiredInfo = None + licenseLabel = None + extendedLicenseLabel = [] + bitstream = None + + def __init__(self, api_resource=None): + super(License, self).__init__(api_resource) + + if api_resource is not None: + self.type = 'clarinlicense' + self.name = api_resource['name'] if 'name' in api_resource else None + self.definition = api_resource['definition'] if 'definition' in api_resource else None + self.confirmation = api_resource['confirmation'] if 'confirmation' in api_resource else 0 + self.requiredInfo = api_resource['requiredInfo'] if 'requiredInfo' in api_resource else None + self.licenseLabel = Label(api_resource['clarinLicenseLabel']) if 'clarinLicenseLabel' in api_resource else None + self.extendedLicenseLabel = [Label(label) for label in api_resource.get('extendedClarinLicenseLabels', [])] + self.bitstream = api_resource['bitstreams'] if 'bitstreams' in api_resource else None + + def to_dict(self): + return { + 'name': self.name, + 'license_id': self.id, + 'definition': self.definition, + 'confirmation': self.confirmation, + 'required_info': self.requiredInfo, + 'label_id': self.licenseLabel.id if self.licenseLabel else None, + } + + +class Label(AddressableHALResource): + """ + Specific attributes and functions for licenses + """ + type = 'clarinlabel' + label = None + title = None + icon = None + extended = False + + def __init__(self, api_resource=None): + """ + Default constructor. Call DSpaceObject init then set label-specific attributes + @param api_resource: API result object to use as initial data + """ + super(Label, self).__init__(api_resource) + + if api_resource is not None: + self.type = 'clarinlicenselabel' + self.label = api_resource['label'] if 'label' in api_resource else None + self.title = api_resource['title'] if 'title' in api_resource else None + self.icon = api_resource['icon'] if 'icon' in api_resource else None + self.extended = api_resource['extended'] if 'extended' in api_resource else None + + def to_dict(self): + return { + 'label_id': self.id, + 'label': self.label, + 'title': self.title, + 'icon': self.icon, + 'is_extended': self.extended + } From dc698dd95e400a8e88de1ae9b6112cb4d698fb4c Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Mon, 28 Oct 2024 22:53:58 +0100 Subject: [PATCH 03/22] removed metadata --- dspace_rest_client/client.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 591b9fd..8360331 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -947,6 +947,26 @@ def add_metadata(self, dso, field, value, language=None, authority=None, confide return dso_type(api_resource=parse_json(r)) + def remove_metadata(self, dso, field, place): + """ + Remove metadata from dso based on metadata field. + """ + if dso is None or field is None or place is None or not isinstance(dso, DSpaceObject): + # TODO: separate these tests, and add better error handling + logging.error('Invalid or missing DSpace object, field or value string') + return self + dso_type = type(dso) + + # Place can be 0+ integer, or a hyphen - meaning "last" + path = f'/metadata/{field}/{place}' + + url = dso.links['self']['href'] + + r = self.api_patch( + url=url, operation=self.PatchOperation.REMOVE, path=path, value=None) + + return dso_type(api_resource=parse_json(r)) + def create_user(self, user, token=None): """ Create a user From 70efb5ffcc8617c7f896a749c1f70b2c048ffa93 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Wed, 13 Nov 2024 14:51:08 +0100 Subject: [PATCH 04/22] remove unused method --- dspace_rest_client/models.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index 6a4679d..8687ee2 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -517,34 +517,6 @@ class RelationshipType(AddressableHALResource): def __init__(self, api_resource): super(RelationshipType, self).__init__(api_resource) - def get_metadata_values(self, field): - """ - Return metadata values as simple list of strings - @param field: DSpace field, eg. dc.creator - @return: list of strings - """ - values = list() - if field in self.metadata: - values = self.metadata[field] - return values - - def as_dict(self): - """ - Return a dict representation of this Item, based on super with item-specific attributes added - @return: dict of Item for API use - """ - dso_dict = super(Item, self).as_dict() - item_dict = {'inArchive': self.inArchive, 'discoverable': self.discoverable, 'withdrawn': self.withdrawn} - return {**dso_dict, **item_dict} - - @classmethod - def from_dso(cls, dso: DSpaceObject): - # Create new Item and copy everything over from this dso - item = cls() - for key, value in dso.__dict__.items(): - item.__dict__[key] = value - return item - class License(AddressableHALResource): """ Specific attributes and functions for licenses From 1ec9e7fc0c0fa62e3f4a2014047ebb1f6d3319cc Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 21 Nov 2024 07:24:37 +0100 Subject: [PATCH 05/22] correct attribute initialization and using --- dspace_rest_client/models.py | 45 +++++++++++------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index 8687ee2..3a43cf3 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -521,27 +521,17 @@ class License(AddressableHALResource): """ Specific attributes and functions for licenses """ - type = 'clarinlicense' - name = None - definition = None - confirmation = 0 - requiredInfo = None - licenseLabel = None - extendedLicenseLabel = [] - bitstream = None - def __init__(self, api_resource=None): super(License, self).__init__(api_resource) - - if api_resource is not None: - self.type = 'clarinlicense' - self.name = api_resource['name'] if 'name' in api_resource else None - self.definition = api_resource['definition'] if 'definition' in api_resource else None - self.confirmation = api_resource['confirmation'] if 'confirmation' in api_resource else 0 - self.requiredInfo = api_resource['requiredInfo'] if 'requiredInfo' in api_resource else None - self.licenseLabel = Label(api_resource['clarinLicenseLabel']) if 'clarinLicenseLabel' in api_resource else None + self.type = 'clarinlicense' + if api_resource: + self.name = api_resource.get('name') + self.definition = api_resource.get('definition') + self.confirmation = api_resource.get('confirmation', 0) + self.requiredInfo = api_resource.get('requiredInfo') + self.licenseLabel = Label(api_resource.get('clarinLicenseLabel')) self.extendedLicenseLabel = [Label(label) for label in api_resource.get('extendedClarinLicenseLabels', [])] - self.bitstream = api_resource['bitstreams'] if 'bitstreams' in api_resource else None + self.bitstream = api_resource.get('bitstreams') def to_dict(self): return { @@ -558,25 +548,18 @@ class Label(AddressableHALResource): """ Specific attributes and functions for licenses """ - type = 'clarinlabel' - label = None - title = None - icon = None - extended = False - def __init__(self, api_resource=None): """ Default constructor. Call DSpaceObject init then set label-specific attributes @param api_resource: API result object to use as initial data """ super(Label, self).__init__(api_resource) - - if api_resource is not None: - self.type = 'clarinlicenselabel' - self.label = api_resource['label'] if 'label' in api_resource else None - self.title = api_resource['title'] if 'title' in api_resource else None - self.icon = api_resource['icon'] if 'icon' in api_resource else None - self.extended = api_resource['extended'] if 'extended' in api_resource else None + self.type = 'clarinlicenselabel' + if api_resource: + self.label = api_resource.get('label') + self.title = api_resource.get('title') + self.icon = api_resource.get('icon') + self.extended = api_resource.get('extended', False) def to_dict(self): return { From 627996b05852c7f0627f12e455704660df0b2ca8 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 21 Nov 2024 07:39:12 +0100 Subject: [PATCH 06/22] correct attribute initialization --- dspace_rest_client/models.py | 45 +++++++++++------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index 8687ee2..3a43cf3 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -521,27 +521,17 @@ class License(AddressableHALResource): """ Specific attributes and functions for licenses """ - type = 'clarinlicense' - name = None - definition = None - confirmation = 0 - requiredInfo = None - licenseLabel = None - extendedLicenseLabel = [] - bitstream = None - def __init__(self, api_resource=None): super(License, self).__init__(api_resource) - - if api_resource is not None: - self.type = 'clarinlicense' - self.name = api_resource['name'] if 'name' in api_resource else None - self.definition = api_resource['definition'] if 'definition' in api_resource else None - self.confirmation = api_resource['confirmation'] if 'confirmation' in api_resource else 0 - self.requiredInfo = api_resource['requiredInfo'] if 'requiredInfo' in api_resource else None - self.licenseLabel = Label(api_resource['clarinLicenseLabel']) if 'clarinLicenseLabel' in api_resource else None + self.type = 'clarinlicense' + if api_resource: + self.name = api_resource.get('name') + self.definition = api_resource.get('definition') + self.confirmation = api_resource.get('confirmation', 0) + self.requiredInfo = api_resource.get('requiredInfo') + self.licenseLabel = Label(api_resource.get('clarinLicenseLabel')) self.extendedLicenseLabel = [Label(label) for label in api_resource.get('extendedClarinLicenseLabels', [])] - self.bitstream = api_resource['bitstreams'] if 'bitstreams' in api_resource else None + self.bitstream = api_resource.get('bitstreams') def to_dict(self): return { @@ -558,25 +548,18 @@ class Label(AddressableHALResource): """ Specific attributes and functions for licenses """ - type = 'clarinlabel' - label = None - title = None - icon = None - extended = False - def __init__(self, api_resource=None): """ Default constructor. Call DSpaceObject init then set label-specific attributes @param api_resource: API result object to use as initial data """ super(Label, self).__init__(api_resource) - - if api_resource is not None: - self.type = 'clarinlicenselabel' - self.label = api_resource['label'] if 'label' in api_resource else None - self.title = api_resource['title'] if 'title' in api_resource else None - self.icon = api_resource['icon'] if 'icon' in api_resource else None - self.extended = api_resource['extended'] if 'extended' in api_resource else None + self.type = 'clarinlicenselabel' + if api_resource: + self.label = api_resource.get('label') + self.title = api_resource.get('title') + self.icon = api_resource.get('icon') + self.extended = api_resource.get('extended', False) def to_dict(self): return { From 03edcbffe637bfaee191fec9a29393822c7c792a Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Tue, 26 Nov 2024 07:31:24 +0100 Subject: [PATCH 07/22] fix api_resource when is None --- dspace_rest_client/models.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index 3a43cf3..a13b083 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -524,14 +524,13 @@ class License(AddressableHALResource): def __init__(self, api_resource=None): super(License, self).__init__(api_resource) self.type = 'clarinlicense' - if api_resource: - self.name = api_resource.get('name') - self.definition = api_resource.get('definition') - self.confirmation = api_resource.get('confirmation', 0) - self.requiredInfo = api_resource.get('requiredInfo') - self.licenseLabel = Label(api_resource.get('clarinLicenseLabel')) - self.extendedLicenseLabel = [Label(label) for label in api_resource.get('extendedClarinLicenseLabels', [])] - self.bitstream = api_resource.get('bitstreams') + self.name = (api_resource or {}).get('name') + self.definition = (api_resource or {}).get('definition') + self.confirmation = (api_resource or {}).get('confirmation', 0) + self.requiredInfo = (api_resource or {}).get('requiredInfo') + self.licenseLabel = Label((api_resource or {}).get('clarinLicenseLabel')) + self.extendedLicenseLabel = [Label(label) for label in (api_resource or {}).get('extendedClarinLicenseLabels', [])] + self.bitstream = (api_resource or {}).get('bitstreams') def to_dict(self): return { @@ -555,11 +554,10 @@ def __init__(self, api_resource=None): """ super(Label, self).__init__(api_resource) self.type = 'clarinlicenselabel' - if api_resource: - self.label = api_resource.get('label') - self.title = api_resource.get('title') - self.icon = api_resource.get('icon') - self.extended = api_resource.get('extended', False) + self.label = (api_resource or {}).get('label') + self.title = (api_resource or {}).get('title') + self.icon = (api_resource or {}).get('icon') + self.extended = (api_resource or {}).get('extended', False) def to_dict(self): return { From 3b15bcaabcf1dabde466e38f7bed7ea7c99d9c71 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Tue, 26 Nov 2024 07:44:21 +0100 Subject: [PATCH 08/22] fix creation Label from None --- dspace_rest_client/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index a13b083..1f80986 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -528,8 +528,11 @@ def __init__(self, api_resource=None): self.definition = (api_resource or {}).get('definition') self.confirmation = (api_resource or {}).get('confirmation', 0) self.requiredInfo = (api_resource or {}).get('requiredInfo') - self.licenseLabel = Label((api_resource or {}).get('clarinLicenseLabel')) - self.extendedLicenseLabel = [Label(label) for label in (api_resource or {}).get('extendedClarinLicenseLabels', [])] + self.licenseLabel = Label(license_label_value) \ + if (license_label_value := (api_resource or {}).get('clarinLicenseLabel')) \ + else None + self.extendedLicenseLabel = [Label(label) for label in + (api_resource or {}).get('extendedClarinLicenseLabels', [])] self.bitstream = (api_resource or {}).get('bitstreams') def to_dict(self): From 2b8ac08ffab180f932077d3659bade5684ffbe94 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Wed, 27 Nov 2024 07:30:24 +0100 Subject: [PATCH 09/22] don't use := and api_resource check before using --- dspace_rest_client/models.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index 1f80986..b62bd52 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -523,17 +523,17 @@ class License(AddressableHALResource): """ def __init__(self, api_resource=None): super(License, self).__init__(api_resource) + api_resource = api_resource or {} self.type = 'clarinlicense' - self.name = (api_resource or {}).get('name') - self.definition = (api_resource or {}).get('definition') - self.confirmation = (api_resource or {}).get('confirmation', 0) - self.requiredInfo = (api_resource or {}).get('requiredInfo') - self.licenseLabel = Label(license_label_value) \ - if (license_label_value := (api_resource or {}).get('clarinLicenseLabel')) \ - else None + self.name = api_resource.get('name') + self.definition = api_resource.get('definition') + self.confirmation = api_resource.get('confirmation', 0) + self.requiredInfo = api_resource.get('requiredInfo') + license_label_value = api_resource.get('clarinLicenseLabel') + self.licenseLabel = Label(license_label_value) if license_label_value else None self.extendedLicenseLabel = [Label(label) for label in - (api_resource or {}).get('extendedClarinLicenseLabels', [])] - self.bitstream = (api_resource or {}).get('bitstreams') + api_resource.get('extendedClarinLicenseLabels', [])] + self.bitstream = api_resource.get('bitstreams') def to_dict(self): return { @@ -556,11 +556,12 @@ def __init__(self, api_resource=None): @param api_resource: API result object to use as initial data """ super(Label, self).__init__(api_resource) + api_resource = api_resource or {} self.type = 'clarinlicenselabel' - self.label = (api_resource or {}).get('label') - self.title = (api_resource or {}).get('title') - self.icon = (api_resource or {}).get('icon') - self.extended = (api_resource or {}).get('extended', False) + self.label = api_resource.get('label') + self.title = api_resource.get('title') + self.icon = api_resource.get('icon') + self.extended = api_resource.get('extended', False) def to_dict(self): return { From 200f4110eed7b3eaa2eddaba736cedee64d8a564 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Mon, 14 Apr 2025 17:44:52 +0300 Subject: [PATCH 10/22] get user_allowance, get/create user_allowance for bitstream and user, fetch bitstream, fetch bundle, get user by email, get http status, logout --- dspace_rest_client/client.py | 177 ++++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 5 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 8360331..b455f3b 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -109,15 +109,17 @@ def __init__(self, api_endpoint=API_ENDPOINT, username=USERNAME, password=PASSWO self.request_headers = {'Content-type': 'application/json', 'User-Agent': self.USER_AGENT} self.list_request_headers = {'Content-type': 'text/uri-list', 'User-Agent': self.USER_AGENT} - def authenticate(self, retry=False): + def authenticate(self, user=None, password=None, retry=False): """ Authenticate with the DSpace REST API. As with other operations, perform XSRF refreshes when necessary. After POST, check /authn/status and log success if the authenticated json property is true @return: response object """ + user = user or self.USERNAME + password = password or self.PASSWORD # Set headers for requests made during authentication # Get and update CSRF token - r = self.session.post(self.LOGIN_URL, data={'user': self.USERNAME, 'password': self.PASSWORD}, + r = self.session.post(self.LOGIN_URL, data={'user': user, 'password': password}, headers=self.auth_request_headers) self.update_token(r) @@ -131,12 +133,12 @@ def authenticate(self, retry=False): return False else: logging.debug("Retrying request with updated CSRF token") - return self.authenticate(retry=True) + return self.authenticate(user=user, password=password, retry=True) if r.status_code == 401: # 401 Unauthorized # If we get a 401, this means a general authentication failure - logging.error(f'Authentication failure: invalid credentials for user {self.USERNAME}') + logging.error(f'Authentication failure: invalid credentials for user {user}') return False # Update headers with new bearer token if present @@ -148,7 +150,7 @@ def authenticate(self, retry=False): if r.status_code == 200: r_json = parse_json(r) if 'authenticated' in r_json and r_json['authenticated'] is True: - logging.info(f'Authenticated successfully as {self.USERNAME}') + logging.info(f'Authenticated successfully as {user}') return r_json['authenticated'] # Default, return false @@ -702,6 +704,7 @@ def create_bitstream(self, bundle=None, name=None, path=None, mime=None, metadat logging.error(f'Error creating bitstream: {r.status_code}: {r.text}') return None + def download_bitstream(self, uuid=None): """ Download bitstream and return full response object including headers, and content @@ -858,6 +861,24 @@ def get_item(self, uuid): logging.error(f'Invalid item UUID: {uuid}') return None + def get_items_by_handle(self, handle): + if handle is None: + return None + params = { + "handle": handle + } + url = f'{self.API_ENDPOINT}/core/items/search/byHandle' + try: + r = self.api_get(url, params, None) + r_json = parse_json(r) + if '_embedded' in r_json: + if 'items' in r_json['_embedded']: + return r_json['_embedded']['items'] + return None + except ValueError: + logging.error(f'Invalid item handle: {handle}') + return None + def get_items(self): """ Get all archived items for a logged-in administrator. Admin only! Usually you will want to @@ -1128,3 +1149,149 @@ def update_resource_policy_group(self, policy_id, group_uuid): body = f'{self.API_ENDPOINT}/eperson/groups/{group_uuid}' r = self.api_put_uri(url, None, body, False) return r + + def get_clarinlruallowances(self): + """ + Fetch all clarinlruallowances. + """ + url = f'{self.API_ENDPOINT}/core/clarinlruallowances' + try: + response = self.api_get(url) + data = parse_json(response) + allowances = data.get('_embedded', {}).get('clarinlruallowances') + if allowances: + logging.info(f"Fetched {len(allowances)} CLARIN LRU allowances.") + return allowances + logging.warning("No CLARIN LRU allowances found.") + except Exception as e: + logging.error(f"Error fetching CLARIN LRU allowances [{url}]: {e}") + return None + + def get_clarinlruallowances_by_bitstreama_and_user(self, bitstream_uuid, user_uuid): + """ + Fetch user allowances for a specific bitstream and user. + """ + url = f'{self.API_ENDPOINT}/core/clarinlruallowance/search/byBitstreamAndUser' + params = {'bitstreamUUID': bitstream_uuid, 'userUUID': user_uuid} + try: + response = self.api_get(url, params=params) + data = parse_json(response) + allowances = data.get('_embedded', {}).get('clarinlruallowances') + if allowances: + logging.info(f"Found {len(allowances)} user allowance(s).") + return allowances + logging.warning(f"No user allowances found for user: {user_uuid} and bitstream: {bitstream_uuid}") + except Exception as e: + logging.error(f"Error fetching user allowances: {e}") + return None + + + def create_clarinlruallowances(self, bitstream_uuid): + """ + Create clarinlruallowances for a bitstream for logged user + by managing user metadata of bitstream. + """ + url = f'{self.API_ENDPOINT}/core/clarinusermetadata/manage' + params = {'bitstreamUUID': bitstream_uuid} + metadata_payload = [ + {"metadataKey": "NAME", "metadataValue": "Test"} + ] + try: + response = self.api_post(url, json=metadata_payload, params=params) + if response.status_code == 200: + logging.info(f"User metadata access managed for bitstream: {bitstream_uuid}") + return True + logging.warning(f"Failed to manage user metadata: {response.status_code}") + except Exception as e: + logging.error(f"Error managing user metadata: {e}") + return False + + + def logout(self): + """ + Log out from the DSpace session. + """ + try: + response = self.session.post(f'{self.API_ENDPOINT}/authn/logout', headers=self.request_headers) + if response.status_code == 204: + self.session.cookies.clear() + self.session.headers.pop('Authorization', None) + logging.info("Logout successful.") + return True + else: + logging.error(f"Logout failed: {response.status_code} - {response.text}") + except Exception as e: + logging.error(f"Logout error: {e}") + return False + + def get_http_status(self, url): + """ + Check the HTTP status code of a URL. + """ + if not url: + logging.warning("Provided URL is not defined.") + return None + try: + response = self.api_get(url) + logging.info(f"Checked URL status: {url} -> {response.status_code}") + return response.status_code + except Exception as e: + logging.error(f"Error getting URL status for {url}: {e}") + return None + + + def fetch_bundle(self, bundle_reference): + """ + Fetch a bundle either by UUID or URL. + """ + if not bundle_reference: + logging.warning("No bundle reference provided.") + return None + + url = bundle_reference if "bundle" in bundle_reference else\ + f'{self.API_ENDPOINT}/core/{bundle_reference}/bundle' + try: + response = self.api_get(url) + data = parse_json(response) + bundle = data.get('_embedded', {}).get('bundles', [{}])[0] + logging.info(f"Bundle retrieved: {bundle.get('uuid', 'unknown')}") + return bundle + except Exception as e: + logging.error(f"Error fetching bundle: {e}") + return None + + def fetch_bitstreams(self, bitstream_reference): + """ + Fetch bitstreams either by UUID or direct URL. + """ + if not bitstream_reference: + logging.warning("No bitstream reference provided.") + return None + + url = bitstream_reference if "bitstream" in bitstream_reference else \ + f'{self.API_ENDPOINT}/core/{bitstream_reference}/bitstream' + try: + response = self.api_get(url) + data = parse_json(response) + bitstreams = data.get('_embedded', {}).get('bitstreams', []) + logging.info(f"Fetched {len(bitstreams)} bitstream(s).") + return bitstreams + except Exception as e: + logging.error(f"Error fetching bitstreams: {e}") + return None + + + def get_user_by_email(self, email): + """ + Retrieve user details using their email address. + """ + url = f'{self.API_ENDPOINT}/eperson/epersons/search/byEmail' + params = {'email': email} + try: + response = self.api_get(url, params=params) + user_data = parse_json(response) + logging.info(f"User fetched by email: {email}") + return User(user_data) + except Exception as e: + logging.error(f"Error retrieving user by email {email}: {e}") + return None From 7f8212437b53f49fcf9631e15223ed617c9581ac Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Mon, 14 Apr 2025 18:08:25 +0300 Subject: [PATCH 11/22] removed empty line --- dspace_rest_client/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index b455f3b..3345b94 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -704,7 +704,6 @@ def create_bitstream(self, bundle=None, name=None, path=None, mime=None, metadat logging.error(f'Error creating bitstream: {r.status_code}: {r.text}') return None - def download_bitstream(self, uuid=None): """ Download bitstream and return full response object including headers, and content From de8737b2cb507b22f9a14c6399f8e942bd22a72d Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Tue, 15 Apr 2025 14:34:32 +0300 Subject: [PATCH 12/22] fix name, logs, doc --- dspace_rest_client/client.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 9f40002..601e3aa 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1201,7 +1201,7 @@ def get_clarinlruallowances(self): logging.error(f"Error fetching CLARIN LRU allowances [{url}]: {e}") return None - def get_clarinlruallowances_by_bitstreama_and_user(self, bitstream_uuid, user_uuid): + def get_clarinlruallowances_by_bitstream_and_user(self, bitstream_uuid, user_uuid): """ Fetch user allowances for a specific bitstream and user. """ @@ -1260,7 +1260,7 @@ def logout(self): def get_http_status(self, url): """ - Check the HTTP status code of a URL. + Get the HTTP status code of a URL. """ if not url: logging.warning("Provided URL is not defined.") @@ -1274,9 +1274,9 @@ def get_http_status(self, url): return None - def fetch_bundle(self, bundle_reference): + def get_bundle(self, bundle_reference): """ - Fetch a bundle either by UUID or URL. + Get a bundle either by UUID or URL. """ if not bundle_reference: logging.warning("No bundle reference provided.") @@ -1287,16 +1287,17 @@ def fetch_bundle(self, bundle_reference): try: response = self.api_get(url) data = parse_json(response) - bundle = data.get('_embedded', {}).get('bundles', [{}])[0] + bundles = data.get('_embedded', {}).get('bundles', []) + bundle = bundles[0] if bundles else {} logging.info(f"Bundle retrieved: {bundle.get('uuid', 'unknown')}") return bundle except Exception as e: - logging.error(f"Error fetching bundle: {e}") + logging.error(f"Error getting bundle: {e}") return None - def fetch_bitstreams(self, bitstream_reference): + def get_bitstream(self, bitstream_reference): """ - Fetch bitstreams either by UUID or direct URL. + Get bitstream either by UUID or direct URL. """ if not bitstream_reference: logging.warning("No bitstream reference provided.") @@ -1308,10 +1309,11 @@ def fetch_bitstreams(self, bitstream_reference): response = self.api_get(url) data = parse_json(response) bitstreams = data.get('_embedded', {}).get('bitstreams', []) - logging.info(f"Fetched {len(bitstreams)} bitstream(s).") - return bitstreams + bitstream = bitstreams[0] if bitstreams else {} + logging.info(f"Bitstream retrieved: {bitstream.get('uuid', 'unknown')}") + return bitstream except Exception as e: - logging.error(f"Error fetching bitstreams: {e}") + logging.error(f"Error getting bitstream: {e}") return None From 48d526839c5212bcf8d6743e3e400570d389a2e2 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Tue, 15 Apr 2025 16:03:33 +0300 Subject: [PATCH 13/22] use _logger instead of logging --- dspace_rest_client/client.py | 55 +++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 601e3aa..b565cd2 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -137,13 +137,13 @@ def authenticate(self, user=None, password=None, retry=False): _logger.error(f'Too many retries updating token: {r.status_code}: {r.text}') return False else: - logging.debug("Retrying request with updated CSRF token") + _logger.debug("Retrying request with updated CSRF token") return self.authenticate(user=user, password=password, retry=True) if r.status_code == 401: # 401 Unauthorized # If we get a 401, this means a general authentication failure - logging.error(f'Authentication failure: invalid credentials for user {user}') + _logger.error(f'Authentication failure: invalid credentials for user {user}') return False # Update headers with new bearer token if present @@ -155,7 +155,7 @@ def authenticate(self, user=None, password=None, retry=False): if r.status_code == 200: r_json = parse_json(r) if 'authenticated' in r_json and r_json['authenticated'] is True: - logging.info(f'Authenticated successfully as {user}') + _logger.info(f'Authenticated successfully as {user}') return r_json['authenticated'] # Default, return false @@ -896,6 +896,9 @@ def get_item(self, uuid): return None def get_items_by_handle(self, handle): + """ + Get items based on handle. + """ if handle is None: return None params = { @@ -910,7 +913,7 @@ def get_items_by_handle(self, handle): return r_json['_embedded']['items'] return None except ValueError: - logging.error(f'Invalid item handle: {handle}') + _logger.error(f'Invalid item handle: {handle}') return None def get_items(self): @@ -1194,11 +1197,11 @@ def get_clarinlruallowances(self): data = parse_json(response) allowances = data.get('_embedded', {}).get('clarinlruallowances') if allowances: - logging.info(f"Fetched {len(allowances)} CLARIN LRU allowances.") + _logger.info(f"Fetched {len(allowances)} CLARIN LRU allowances.") return allowances - logging.warning("No CLARIN LRU allowances found.") + _logger.warning("No CLARIN LRU allowances found.") except Exception as e: - logging.error(f"Error fetching CLARIN LRU allowances [{url}]: {e}") + _logger.error(f"Error fetching CLARIN LRU allowances [{url}]: {e}") return None def get_clarinlruallowances_by_bitstream_and_user(self, bitstream_uuid, user_uuid): @@ -1212,11 +1215,11 @@ def get_clarinlruallowances_by_bitstream_and_user(self, bitstream_uuid, user_uui data = parse_json(response) allowances = data.get('_embedded', {}).get('clarinlruallowances') if allowances: - logging.info(f"Found {len(allowances)} user allowance(s).") + _logger.info(f"Found {len(allowances)} user allowance(s).") return allowances - logging.warning(f"No user allowances found for user: {user_uuid} and bitstream: {bitstream_uuid}") + _logger.warning(f"No user allowances found for user: {user_uuid} and bitstream: {bitstream_uuid}") except Exception as e: - logging.error(f"Error fetching user allowances: {e}") + _logger.error(f"Error fetching user allowances: {e}") return None @@ -1233,11 +1236,11 @@ def create_clarinlruallowances(self, bitstream_uuid): try: response = self.api_post(url, json=metadata_payload, params=params) if response.status_code == 200: - logging.info(f"User metadata access managed for bitstream: {bitstream_uuid}") + _logger.info(f"User metadata access managed for bitstream: {bitstream_uuid}") return True - logging.warning(f"Failed to manage user metadata: {response.status_code}") + _logger.warning(f"Failed to manage user metadata: {response.status_code}") except Exception as e: - logging.error(f"Error managing user metadata: {e}") + _logger.error(f"Error managing user metadata: {e}") return False @@ -1250,12 +1253,12 @@ def logout(self): if response.status_code == 204: self.session.cookies.clear() self.session.headers.pop('Authorization', None) - logging.info("Logout successful.") + _logger.info("Logout successful.") return True else: - logging.error(f"Logout failed: {response.status_code} - {response.text}") + _logger.error(f"Logout failed: {response.status_code} - {response.text}") except Exception as e: - logging.error(f"Logout error: {e}") + _logger.error(f"Logout error: {e}") return False def get_http_status(self, url): @@ -1263,14 +1266,14 @@ def get_http_status(self, url): Get the HTTP status code of a URL. """ if not url: - logging.warning("Provided URL is not defined.") + _logger.warning("Provided URL is not defined.") return None try: response = self.api_get(url) - logging.info(f"Checked URL status: {url} -> {response.status_code}") + _logger.info(f"Checked URL status: {url} -> {response.status_code}") return response.status_code except Exception as e: - logging.error(f"Error getting URL status for {url}: {e}") + _logger.error(f"Error getting URL status for {url}: {e}") return None @@ -1289,10 +1292,10 @@ def get_bundle(self, bundle_reference): data = parse_json(response) bundles = data.get('_embedded', {}).get('bundles', []) bundle = bundles[0] if bundles else {} - logging.info(f"Bundle retrieved: {bundle.get('uuid', 'unknown')}") + _logger.info(f"Bundle retrieved: {bundle.get('uuid', 'unknown')}") return bundle except Exception as e: - logging.error(f"Error getting bundle: {e}") + _logger.error(f"Error getting bundle: {e}") return None def get_bitstream(self, bitstream_reference): @@ -1300,7 +1303,7 @@ def get_bitstream(self, bitstream_reference): Get bitstream either by UUID or direct URL. """ if not bitstream_reference: - logging.warning("No bitstream reference provided.") + _logger.warning("No bitstream reference provided.") return None url = bitstream_reference if "bitstream" in bitstream_reference else \ @@ -1310,10 +1313,10 @@ def get_bitstream(self, bitstream_reference): data = parse_json(response) bitstreams = data.get('_embedded', {}).get('bitstreams', []) bitstream = bitstreams[0] if bitstreams else {} - logging.info(f"Bitstream retrieved: {bitstream.get('uuid', 'unknown')}") + _logger.info(f"Bitstream retrieved: {bitstream.get('uuid', 'unknown')}") return bitstream except Exception as e: - logging.error(f"Error getting bitstream: {e}") + _logger.error(f"Error getting bitstream: {e}") return None @@ -1326,8 +1329,8 @@ def get_user_by_email(self, email): try: response = self.api_get(url, params=params) user_data = parse_json(response) - logging.info(f"User fetched by email: {email}") + _logger.info(f"User fetched by email: {email}") return User(user_data) except Exception as e: - logging.error(f"Error retrieving user by email {email}: {e}") + _logger.error(f"Error retrieving user by email {email}: {e}") return None From 9abe27000c296b2a21967a9a635705a994cfd537 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Wed, 16 Apr 2025 11:33:16 +0300 Subject: [PATCH 14/22] removed unneeded methods, add warning when user is not login --- dspace_rest_client/client.py | 53 +++++------------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index b565cd2..66f8cac 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -895,7 +895,7 @@ def get_item(self, uuid): _logger.error(f'Invalid item UUID: {uuid}') return None - def get_items_by_handle(self, handle): + def get_item_by_handle(self, handle): """ Get items based on handle. """ @@ -910,7 +910,9 @@ def get_items_by_handle(self, handle): r_json = parse_json(r) if '_embedded' in r_json: if 'items' in r_json['_embedded']: - return r_json['_embedded']['items'] + items = r_json['_embedded']['items'] + if len(items) > 0: + return Item(items[0]) return None except ValueError: _logger.error(f'Invalid item handle: {handle}') @@ -1255,6 +1257,9 @@ def logout(self): self.session.headers.pop('Authorization', None) _logger.info("Logout successful.") return True + elif response.status_code == 403 and 'Invalid CSRF token' in response.text: + _logger.warning("Logout skipped: not logged in or invalid CSRF token.") + return True else: _logger.error(f"Logout failed: {response.status_code} - {response.text}") except Exception as e: @@ -1276,50 +1281,6 @@ def get_http_status(self, url): _logger.error(f"Error getting URL status for {url}: {e}") return None - - def get_bundle(self, bundle_reference): - """ - Get a bundle either by UUID or URL. - """ - if not bundle_reference: - logging.warning("No bundle reference provided.") - return None - - url = bundle_reference if "bundle" in bundle_reference else\ - f'{self.API_ENDPOINT}/core/{bundle_reference}/bundle' - try: - response = self.api_get(url) - data = parse_json(response) - bundles = data.get('_embedded', {}).get('bundles', []) - bundle = bundles[0] if bundles else {} - _logger.info(f"Bundle retrieved: {bundle.get('uuid', 'unknown')}") - return bundle - except Exception as e: - _logger.error(f"Error getting bundle: {e}") - return None - - def get_bitstream(self, bitstream_reference): - """ - Get bitstream either by UUID or direct URL. - """ - if not bitstream_reference: - _logger.warning("No bitstream reference provided.") - return None - - url = bitstream_reference if "bitstream" in bitstream_reference else \ - f'{self.API_ENDPOINT}/core/{bitstream_reference}/bitstream' - try: - response = self.api_get(url) - data = parse_json(response) - bitstreams = data.get('_embedded', {}).get('bitstreams', []) - bitstream = bitstreams[0] if bitstreams else {} - _logger.info(f"Bitstream retrieved: {bitstream.get('uuid', 'unknown')}") - return bitstream - except Exception as e: - _logger.error(f"Error getting bitstream: {e}") - return None - - def get_user_by_email(self, email): """ Retrieve user details using their email address. From 537ca1743b211376a2edd5acf7b0d5555ddc2722 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Wed, 16 Apr 2025 11:35:20 +0300 Subject: [PATCH 15/22] fix grammer --- dspace_rest_client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 66f8cac..d17e696 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -897,7 +897,7 @@ def get_item(self, uuid): def get_item_by_handle(self, handle): """ - Get items based on handle. + Get item based on handle. """ if handle is None: return None From d70ebd70b99162395e4916de3a27eca0bdfe36ad Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 24 Apr 2025 10:10:11 +0300 Subject: [PATCH 16/22] removed authorization by another user, removed unneeded methods --- dspace_rest_client/client.py | 49 +++++------------------------------- 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index d17e696..965546b 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -114,17 +114,15 @@ def __init__(self, api_endpoint=API_ENDPOINT, username=USERNAME, password=PASSWO self.request_headers = {'Content-type': 'application/json', 'User-Agent': self.USER_AGENT} self.list_request_headers = {'Content-type': 'text/uri-list', 'User-Agent': self.USER_AGENT} - def authenticate(self, user=None, password=None, retry=False): + def authenticate(self, retry=False): """ Authenticate with the DSpace REST API. As with other operations, perform XSRF refreshes when necessary. After POST, check /authn/status and log success if the authenticated json property is true @return: response object """ - user = user or self.USERNAME - password = password or self.PASSWORD # Set headers for requests made during authentication # Get and update CSRF token - r = self.session.post(self.LOGIN_URL, data={'user': user, 'password': password}, + r = self.session.post(self.LOGIN_URL, data={'user': self.USERNAME, 'password': self.PASSWORD}, headers=self.auth_request_headers) self.update_token(r) @@ -138,12 +136,12 @@ def authenticate(self, user=None, password=None, retry=False): return False else: _logger.debug("Retrying request with updated CSRF token") - return self.authenticate(user=user, password=password, retry=True) + return self.authenticate(retry=True) if r.status_code == 401: # 401 Unauthorized # If we get a 401, this means a general authentication failure - _logger.error(f'Authentication failure: invalid credentials for user {user}') + _logger.error(f'Authentication failure: invalid credentials for user {self.USERNAME}') return False # Update headers with new bearer token if present @@ -155,7 +153,7 @@ def authenticate(self, user=None, password=None, retry=False): if r.status_code == 200: r_json = parse_json(r) if 'authenticated' in r_json and r_json['authenticated'] is True: - _logger.info(f'Authenticated successfully as {user}') + _logger.info(f'Authenticated successfully as {self.USERNAME}') return r_json['authenticated'] # Default, return false @@ -914,7 +912,7 @@ def get_item_by_handle(self, handle): if len(items) > 0: return Item(items[0]) return None - except ValueError: + except (TypeError, ValueError): _logger.error(f'Invalid item handle: {handle}') return None @@ -1246,41 +1244,6 @@ def create_clarinlruallowances(self, bitstream_uuid): return False - def logout(self): - """ - Log out from the DSpace session. - """ - try: - response = self.session.post(f'{self.API_ENDPOINT}/authn/logout', headers=self.request_headers) - if response.status_code == 204: - self.session.cookies.clear() - self.session.headers.pop('Authorization', None) - _logger.info("Logout successful.") - return True - elif response.status_code == 403 and 'Invalid CSRF token' in response.text: - _logger.warning("Logout skipped: not logged in or invalid CSRF token.") - return True - else: - _logger.error(f"Logout failed: {response.status_code} - {response.text}") - except Exception as e: - _logger.error(f"Logout error: {e}") - return False - - def get_http_status(self, url): - """ - Get the HTTP status code of a URL. - """ - if not url: - _logger.warning("Provided URL is not defined.") - return None - try: - response = self.api_get(url) - _logger.info(f"Checked URL status: {url} -> {response.status_code}") - return response.status_code - except Exception as e: - _logger.error(f"Error getting URL status for {url}: {e}") - return None - def get_user_by_email(self, email): """ Retrieve user details using their email address. From 3f4d9ec3bef6b17329c66584a1bf438eb83f5c5f Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Mon, 28 Apr 2025 15:29:11 +0300 Subject: [PATCH 17/22] removed logging --- dspace_rest_client/client.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 965546b..e5ab117 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1197,9 +1197,7 @@ def get_clarinlruallowances(self): data = parse_json(response) allowances = data.get('_embedded', {}).get('clarinlruallowances') if allowances: - _logger.info(f"Fetched {len(allowances)} CLARIN LRU allowances.") return allowances - _logger.warning("No CLARIN LRU allowances found.") except Exception as e: _logger.error(f"Error fetching CLARIN LRU allowances [{url}]: {e}") return None @@ -1215,9 +1213,7 @@ def get_clarinlruallowances_by_bitstream_and_user(self, bitstream_uuid, user_uui data = parse_json(response) allowances = data.get('_embedded', {}).get('clarinlruallowances') if allowances: - _logger.info(f"Found {len(allowances)} user allowance(s).") return allowances - _logger.warning(f"No user allowances found for user: {user_uuid} and bitstream: {bitstream_uuid}") except Exception as e: _logger.error(f"Error fetching user allowances: {e}") return None @@ -1236,9 +1232,7 @@ def create_clarinlruallowances(self, bitstream_uuid): try: response = self.api_post(url, json=metadata_payload, params=params) if response.status_code == 200: - _logger.info(f"User metadata access managed for bitstream: {bitstream_uuid}") return True - _logger.warning(f"Failed to manage user metadata: {response.status_code}") except Exception as e: _logger.error(f"Error managing user metadata: {e}") return False @@ -1253,7 +1247,6 @@ def get_user_by_email(self, email): try: response = self.api_get(url, params=params) user_data = parse_json(response) - _logger.info(f"User fetched by email: {email}") return User(user_data) except Exception as e: _logger.error(f"Error retrieving user by email {email}: {e}") From 342cfc5f7b6f21c922abc60df7f8e3546b0baa74 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Tue, 20 May 2025 15:02:02 +0300 Subject: [PATCH 18/22] method for adding eperson to group as its member --- dspace_rest_client/client.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index e5ab117..501b918 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1082,6 +1082,35 @@ def create_group(self, group): # that you see for other DSO types - still figuring out the best way return Group(api_resource=parse_json(self.create_dso(url, params=None, data=data))) + def add_member(self, group, eperson): + """ + Adds a user (EPerson) as a member of the specified group. + + Args: + group (Group): The group to which the user will be added. + eperson (User): The EPerson to be added as a member of the group. + + Returns: + bool: True if the user was successfully added (HTTP 204), False otherwise. + """ + if not isinstance(group, Group): + _logger.error("Provided 'group' is not an instance of Group.") + return False + + if not isinstance(eperson, User): + _logger.error("Provided 'eperson' is not an instance of User.") + return False + + url = f' {self.API_ENDPOINT}/eperson/groups/{group.uuid}/epersons' + eperson_uri = f'{self.API_ENDPOINT}/epersons/{eperson.uuid}' + r = self.api_post_uri(url, params=None, uri_list=eperson_uri) + if r.status_code == 204: + return True + _logger.error(f"Failed to add user {eperson.uuid} to group {group.uuid}. " + f"Status code: {r.status_code}") + return False + + def start_workflow(self, workspace_item): url = f'{self.API_ENDPOINT}/workflow/workflowitems' res = parse_json(self.api_post_uri(url, params=None, uri_list=workspace_item)) From 2650a4645a337bc2ee839aa3d0e226ac7428a311 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 22 May 2025 11:44:55 +0300 Subject: [PATCH 19/22] create submit group, create resource policy --- dspace_rest_client/client.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 501b918..9176c71 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1082,6 +1082,16 @@ def create_group(self, group): # that you see for other DSO types - still figuring out the best way return Group(api_resource=parse_json(self.create_dso(url, params=None, data=data))) + def create_submit_group(self, collection): + """ + Creates a submitter group for the given collection. + """ + url = f'{self.API_ENDPOINT}/core/collections/{collection.uuid}/submittersGroup' + r = self.api_post(url, json={}, params=None) + if r.status_code == 201: + return Group(parse_json(r)) + return None + def add_member(self, group, eperson): """ Adds a user (EPerson) as a member of the specified group. @@ -1207,6 +1217,23 @@ def get_resource_policy(self, bundle_uuid): if 'resourcepolicies' in r_json['_embedded']: return r_json['_embedded']['resourcepolicies'][0] + def create_resource_policy(self, resource_uuid, data, group_uuid=None, eperson_uuid=None): + """ + Creates a resource policy by sending a POST request to the API endpoint. + """ + url = f'{self.API_ENDPOINT}/authz/resourcepolicies' + params = {"resource": resource_uuid} + if group_uuid: + params["group"] = group_uuid + if eperson_uuid: + params["eperson"] = eperson_uuid + + r = self.api_post(url, params=params, json=data) + if r.status_code == 201: + return True + return False + + def update_resource_policy_group(self, policy_id, group_uuid): """ Update a resource policy with a new group From f7a5f9ece572d9003e08d827034e82a0c8f36d61 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Mon, 9 Jun 2025 12:08:43 +0200 Subject: [PATCH 20/22] removed empty space --- dspace_rest_client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 9176c71..339fd9a 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1111,7 +1111,7 @@ def add_member(self, group, eperson): _logger.error("Provided 'eperson' is not an instance of User.") return False - url = f' {self.API_ENDPOINT}/eperson/groups/{group.uuid}/epersons' + url = f'{self.API_ENDPOINT}/eperson/groups/{group.uuid}/epersons' eperson_uri = f'{self.API_ENDPOINT}/epersons/{eperson.uuid}' r = self.api_post_uri(url, params=None, uri_list=eperson_uri) if r.status_code == 204: From 4a59d83c3fc47b801afa4be3ea273bb85013c2a6 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Tue, 11 Aug 2026 11:17:00 +0200 Subject: [PATCH 21/22] fix: use plural collection name for clarinlruallowances search get_clarinlruallowances_by_bitstream_and_user() requested the singular collection /core/clarinlruallowance/search/byBitstreamAndUser. DSpace 9 routes REST repositories by their declared plural name, so that path returns: 404 {"message": "The repository type core.clarinlruallowance was not found"} The caller treats the empty result as "no allowance exists" and reports NO_ALLOWANCE, which surfaced in dspace-rest-test as 2 failures and 2 errors in test_endpoints against CLARIN DSpace 9.3 (dtq-dev-9-base) -- even though the allowance was present all along. The plural form is backward compatible: verified test_endpoints passes 8/8 against both DSpace 9.3 (dev-6) and DSpace 7.6.5 (dev-5), so no version switch is needed. Note the other two CLARIN URLs in this client are already correct and are left untouched: /core/clarinlruallowances (already plural) and /core/clarinusermetadata/manage (an explicit @RequestMapping that takes precedence over repository routing -- returns 403 on both 7.6.5 and 9.3). Co-Authored-By: Claude Opus 5 (1M context) --- dspace_rest_client/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 339fd9a..6e956e7 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1261,8 +1261,12 @@ def get_clarinlruallowances(self): def get_clarinlruallowances_by_bitstream_and_user(self, bitstream_uuid, user_uuid): """ Fetch user allowances for a specific bitstream and user. + + Note: the collection name must be plural. DSpace 9 routes REST repositories by their + declared plural name, so the singular form returns 404 "The repository type + core.clarinlruallowance was not found". The plural form works on DSpace 7 too. """ - url = f'{self.API_ENDPOINT}/core/clarinlruallowance/search/byBitstreamAndUser' + url = f'{self.API_ENDPOINT}/core/clarinlruallowances/search/byBitstreamAndUser' params = {'bitstreamUUID': bitstream_uuid, 'userUUID': user_uuid} try: response = self.api_get(url, params=params) From 82f9516832f4bcca00748476fb355d63a03cdfab Mon Sep 17 00:00:00 2001 From: Kasinhou <129340513+Kasinhou@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:06:12 +0200 Subject: [PATCH 22/22] Remove note on plural collection names in docstring Removed note about collection name pluralization for DSpace. --- dspace_rest_client/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 6e956e7..cd199fe 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1261,10 +1261,6 @@ def get_clarinlruallowances(self): def get_clarinlruallowances_by_bitstream_and_user(self, bitstream_uuid, user_uuid): """ Fetch user allowances for a specific bitstream and user. - - Note: the collection name must be plural. DSpace 9 routes REST repositories by their - declared plural name, so the singular form returns 404 "The repository type - core.clarinlruallowance was not found". The plural form works on DSpace 7 too. """ url = f'{self.API_ENDPOINT}/core/clarinlruallowances/search/byBitstreamAndUser' params = {'bitstreamUUID': bitstream_uuid, 'userUUID': user_uuid}