From 1b5630446c869e8440a69409b7f4471dc853ecd8 Mon Sep 17 00:00:00 2001 From: jm Date: Mon, 15 Apr 2024 16:29:54 +0200 Subject: [PATCH 01/25] added get_items --- dspace_rest_client/client.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 8ef88ed..938e688 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -687,6 +687,21 @@ def create_collection(self, parent, data): params = {'parent': parent} return Collection(api_resource=parse_json(self.create_dso(url, params, data))) + def get_items(self): + """ + Get all items + @return: list of Item objects + """ + url = f'{self.API_ENDPOINT}/core/items' + items = list() + r = self.api_get(url) + r_json = parse_json(r) + if '_embedded' in r_json: + if 'items' in r_json['_embedded']: + for item_resource in r_json['_embedded']['items']: + items.append(Item(item_resource)) + return items + def get_item(self, uuid): """ Get an item, given its UUID From 7442996c6364c30a28cbf30c6a2d224742188237 Mon Sep 17 00:00:00 2001 From: jm Date: Mon, 15 Apr 2024 16:35:00 +0200 Subject: [PATCH 02/25] do not require pysolr --- dspace_rest_client/client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 107cc94..299c501 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -19,7 +19,6 @@ import requests from requests import Request -import pysolr import os from uuid import UUID from .models import * @@ -95,7 +94,12 @@ def __init__(self, api_endpoint=API_ENDPOINT, username=USERNAME, password=PASSWO self.USERNAME = username self.PASSWORD = password self.SOLR_ENDPOINT = solr_endpoint - self.solr = pysolr.Solr(url=solr_endpoint, always_commit=True, timeout=300, auth=solr_auth) + self.solr = None + try: + import pysolr + self.solr = pysolr.Solr(url=solr_endpoint, always_commit=True, timeout=300, auth=solr_auth) + except Exception: + pass # If fake_user_agent was specified, use this string that is known (as of 2023-12-03) to succeed with # requests to Cloudfront-protected API endpoints (tested on demo.dspace.org) # Otherwise, the user agent will be the more helpful and accurate default of 'DSpace Python REST Client' From 5e7e83163eadb8a28e0ffc0fd786938c3ab5627a Mon Sep 17 00:00:00 2001 From: MajoBerger Date: Fri, 19 Apr 2024 14:57:47 +0200 Subject: [PATCH 03/25] log in again, when logged off by timeout --- dspace_rest_client/client.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 299c501..fa80504 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -207,6 +207,21 @@ def api_post(self, url, params, json, retry=False): logging.debug("Retrying request with updated CSRF token") return self.api_post(url, params=params, json=json, retry=True) + # we need to log in again, if there is login error. This is a bad + # solution copied from the past + elif r.status_code == 401: + r_json = parse_json(r) + if 'message' in r_json and 'Authentication is required' in r_json['message']: + if retry: + logging.error( + 'API Post: Already retried... something must be wrong') + else: + logging.debug("API Post: Retrying request with updated CSRF token") + # try to authenticate + self.authenticate() + # Try to authenticate and repeat the request 3 times - + # if it won't happen log error + return self.api_post(url, params=params, json=json, retry=False) return r def api_post_uri(self, url, params, uri_list, retry=False): From 426a80f3b129a1ec03c5fb50190e3ecf8757ef82 Mon Sep 17 00:00:00 2001 From: jm Date: Wed, 3 Jul 2024 12:29:20 +0200 Subject: [PATCH 04/25] [linter] fix ruff --- console.py | 4 +--- dspace_rest_client/client.py | 18 +++++++++--------- dspace_rest_client/models.py | 6 ------ example.py | 14 +++++++------- example_gets.py | 3 +-- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/console.py b/console.py index 795d6a7..150aaec 100644 --- a/console.py +++ b/console.py @@ -1,7 +1,5 @@ from dspace_rest_client.client import DSpaceClient -from dspace_rest_client.models import Community, Collection, Item, Bundle, Bitstream import code -import logging import os # The DSpace client will look for the same environment variables but we can also look for them here explicitly @@ -22,7 +20,7 @@ # Authenticate against the DSpace client authenticated = d.authenticate() if not authenticated: - print(f'Error logging in! Giving up.') + print('Error logging in! Giving up.') exit(1) code.interact(local=locals()) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 299c501..2ab1886 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -307,15 +307,15 @@ def api_patch(self, url, operation, path, value, retry=False): @see https://github.com/DSpace/RestContract/blob/main/metadata-patch.md """ if url is None: - logging.error(f'Missing required URL argument') + logging.error('Missing required URL argument') return None if path is None: - logging.error(f'Need valid path eg. /withdrawn or /metadata/dc.title/0/language') + logging.error('Need valid path eg. /withdrawn or /metadata/dc.title/0/language') return None if (operation == self.PatchOperation.ADD or operation == self.PatchOperation.REPLACE or operation == self.PatchOperation.MOVE) and value is None: # missing value required for add/replace/move operations - logging.error(f'Missing required "value" argument for add/replace/move operations') + logging.error('Missing required "value" argument for add/replace/move operations') return None # compile patch data @@ -464,8 +464,8 @@ def update_dso(self, dso, params=None): return None dso_type = type(dso) if not isinstance(dso, SimpleDSpaceObject): - logging.error(f'Only SimpleDSpaceObject types (eg Item, Collection, Community) ' - f'are supported by generic update_dso PUT.') + logging.error('Only SimpleDSpaceObject types (eg Item, Collection, Community) ' + 'are supported by generic update_dso PUT.') return dso try: # Get self URI from HAL links @@ -511,12 +511,12 @@ def delete_dso(self, dso=None, url=None, params=None): """ if dso is None: if url is None: - logging.error(f'Need a DSO or a URL to delete') + logging.error('Need a DSO or a URL to delete') return None else: if not isinstance(dso, SimpleDSpaceObject): - logging.error(f'Only SimpleDSpaceObject types (eg Item, Collection, Community, EPerson) ' - f'are supported by generic update_dso PUT.') + logging.error('Only SimpleDSpaceObject types (eg Item, Collection, Community, EPerson) ' + 'are supported by generic update_dso PUT.') return dso # Get self URI from HAL links url = dso.links['self']['href'] @@ -957,7 +957,7 @@ def create_user(self, user, token=None): def delete_user(self, user): if not isinstance(user, User): - logging.error(f'Must be a valid user') + logging.error('Must be a valid user') return None return self.delete_dso(user) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index de463ac..21e3a3c 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -9,14 +9,8 @@ @author Kim Shepherd """ -import code import json -import logging -import requests -from requests import Request -import os -from uuid import UUID __all__ = ['DSpaceObject', 'HALResource', 'ExternalDataObject', 'SimpleDSpaceObject', 'Community', 'Collection', 'Item', 'Bundle', 'Bitstream', 'User', 'Group'] diff --git a/example.py b/example.py index 461078f..c526c07 100644 --- a/example.py +++ b/example.py @@ -31,7 +31,7 @@ # Authenticate against the DSpace client authenticated = d.authenticate() if not authenticated: - print(f'Error logging in! Giving up.') + print('Error logging in! Giving up.') exit(1) # Put together some basic Community data. @@ -58,7 +58,7 @@ if isinstance(new_community, Community) and new_community.uuid is not None: print(f'New community created! Handle: {new_community.handle}') else: - print(f'Error! Giving up.') + print('Error! Giving up.') exit(1) # Update the community metadata @@ -93,7 +93,7 @@ if isinstance(new_collection, Collection) and new_collection.uuid is not None: print(f'New collection created! Handle: {new_collection.handle}') else: - print(f'Error! Giving up.') + print('Error! Giving up.') exit(1) # Put together some basic Item data. @@ -146,7 +146,7 @@ if isinstance(new_item, Item) and new_item.uuid is not None: print(f'New item created! Handle: {new_item.handle}') else: - print(f'Error! Giving up.') + print('Error! Giving up.') exit(1) # Add a single metadata field+value to the item (PATCH operation) @@ -159,7 +159,7 @@ if isinstance(new_bundle, Bundle) and new_bundle.uuid is not None: print(f'New bundle created! UUID: {new_bundle.uuid}') else: - print(f'Error! Giving up.') + print('Error! Giving up.') exit(1) # Create and upload a new bitstream using the LICENSE.txt file in this project @@ -181,10 +181,10 @@ if isinstance(new_bitstream, Bitstream) and new_bitstream.uuid is not None: print(f'New bitstream created! UUID: {new_bitstream.uuid}') else: - print(f'Error! Giving up.') + print('Error! Giving up.') exit(1) -print(f'All finished with example data creation. Visit your test repository to review created objects') +print('All finished with example data creation. Visit your test repository to review created objects') # Retrieving objects - now that we know there is some data in the repository we can demonstrate # some simple ways of retrieving and iterating DSOs diff --git a/example_gets.py b/example_gets.py index a6a6c77..fdd23fc 100644 --- a/example_gets.py +++ b/example_gets.py @@ -7,7 +7,6 @@ """ from dspace_rest_client.client import DSpaceClient -from dspace_rest_client.models import Community, Collection, Item, Bundle, Bitstream # Example variables needed for authentication and basic API requests # SET THESE TO MATCH YOUR TEST SYSTEM BEFORE RUNNING THE EXAMPLE SCRIPT @@ -30,7 +29,7 @@ # Authenticate against the DSpace client authenticated = d.authenticate() if not authenticated: - print(f'Error logging in! Giving up.') + print('Error logging in! Giving up.') exit(1) # Retrieving objects - now that we know there is some data in the repository we can demonstrate From 90bfdc73c813ffe141c5e452163b3295f399f5ba Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Wed, 9 Oct 2024 17:25:09 +0200 Subject: [PATCH 05/25] 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 06/25] 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 6cd46a34095cf52d52fe0fdc119fcf7552136f36 Mon Sep 17 00:00:00 2001 From: jm Date: Thu, 24 Oct 2024 20:23:34 +0200 Subject: [PATCH 07/25] make logging configurable --- dspace_rest_client/client.py | 113 ++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 299c501..a9ec5f0 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -26,6 +26,7 @@ __all__ = ['DSpaceClient'] logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) +_logger = logging.getLogger("dspace.client") def parse_json(response): @@ -38,7 +39,7 @@ def parse_json(response): try: response_json = response.json() except ValueError as err: - logging.error(f'Error parsing response JSON: {err}. Body text: {response.text}') + _logger.error(f'Error parsing response JSON: {err}. Body text: {response.text}') return response_json @@ -131,16 +132,16 @@ def authenticate(self, retry=False): # 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 if retry: - logging.error(f'Too many retries updating token: {r.status_code}: {r.text}') + _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(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}') + _logger.error(f'Authentication failure: invalid credentials for user {self.USERNAME}') return False # Update headers with new bearer token if present @@ -152,7 +153,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}') + _logger.info(f'Authenticated successfully as {self.USERNAME}') return r_json['authenticated'] # Default, return false @@ -202,9 +203,9 @@ def api_post(self, url, params, json, retry=False): 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}') + _logger.warning(f'Too many retries updating token: {r.status_code}: {r.text}') else: - logging.debug("Retrying request with updated CSRF token") + _logger.debug("Retrying request with updated CSRF token") return self.api_post(url, params=params, json=json, retry=True) return r @@ -230,9 +231,9 @@ def api_post_uri(self, url, params, uri_list, retry=False): r_json = r.json() 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}') + _logger.warning(f'Too many retries updating token: {r.status_code}: {r.text}') else: - logging.debug("Retrying request with updated CSRF token") + _logger.debug("Retrying request with updated CSRF token") return self.api_post_uri(url, params=params, uri_list=uri_list, retry=True) return r @@ -255,14 +256,14 @@ def api_put(self, url, params, json, retry=False): # 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) + _logger.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}') + _logger.warning(f'Too many retries updating token: {r.status_code}: {r.text}') else: - logging.debug("Retrying request with updated CSRF token") + _logger.debug("Retrying request with updated CSRF token") return self.api_put(url, params=params, json=json, retry=True) return r @@ -284,14 +285,14 @@ def api_delete(self, url, params, retry=False): # 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) + _logger.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}') + _logger.warning(f'Too many retries updating token: {r.status_code}: {r.text}') else: - logging.debug("Retrying request with updated CSRF token") + _logger.debug("Retrying request with updated CSRF token") return self.api_delete(url, params=params, retry=True) return r @@ -307,15 +308,15 @@ def api_patch(self, url, operation, path, value, retry=False): @see https://github.com/DSpace/RestContract/blob/main/metadata-patch.md """ if url is None: - logging.error(f'Missing required URL argument') + _logger.error(f'Missing required URL argument') return None if path is None: - logging.error(f'Need valid path eg. /withdrawn or /metadata/dc.title/0/language') + _logger.error(f'Need valid path eg. /withdrawn or /metadata/dc.title/0/language') return None if (operation == self.PatchOperation.ADD or operation == self.PatchOperation.REPLACE or operation == self.PatchOperation.MOVE) and value is None: # missing value required for add/replace/move operations - logging.error(f'Missing required "value" argument for add/replace/move operations') + _logger.error(f'Missing required "value" argument for add/replace/move operations') return None # compile patch data @@ -339,17 +340,17 @@ def api_patch(self, url, operation, path, value, retry=False): # 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) + _logger.debug(r.text) 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}') + _logger.warning(f'Too many retries updating token: {r.status_code}: {r.text}') else: - logging.debug("Retrying request with updated CSRF token") + _logger.debug("Retrying request with updated CSRF token") return self.api_patch(url, operation, path, value, True) elif r.status_code == 200: # 200 Success - logging.info(f'successful patch update to {r.json()["type"]} {r.json()["id"]}') + _logger.info(f'successful patch update to {r.json()["type"]} {r.json()["id"]}') # Return the raw API response return r @@ -396,7 +397,7 @@ def search_objects(self, query=None, scope=None, filters=None, page=0, size=20, dso = DSpaceObject(resource) dsos.append(dso) except (TypeError, ValueError) as err: - logging.error(f'error parsing search result json {err}') + _logger.error(f'error parsing search result json {err}') return dsos @@ -410,7 +411,7 @@ def fetch_resource(self, url, params=None): """ r = self.api_get(url, params, None) if r.status_code != 200: - logging.error(f'Error encountered fetching resource: {r.text}') + _logger.error(f'Error encountered fetching resource: {r.text}') return None # ValueError / JSON handling moved to static method return parse_json(r) @@ -429,7 +430,7 @@ def get_dso(self, url, uuid): url = f'{url}/{uuid}' return self.api_get(url, None, None) except ValueError: - logging.error(f'Invalid DSO UUID: {uuid}') + _logger.error(f'Invalid DSO UUID: {uuid}') return None def create_dso(self, url, params, data): @@ -446,9 +447,9 @@ def create_dso(self, url, params, data): if r.status_code == 201: # 201 Created - success! new_dso = parse_json(r) - logging.info(f'{new_dso["type"]} {new_dso["uuid"]} created successfully!') + _logger.info(f'{new_dso["type"]} {new_dso["uuid"]} created successfully!') else: - logging.error(f'create operation failed: {r.status_code}: {r.text} ({url})') + _logger.error(f'create operation failed: {r.status_code}: {r.text} ({url})') return r def update_dso(self, dso, params=None): @@ -464,7 +465,7 @@ def update_dso(self, dso, params=None): return None dso_type = type(dso) if not isinstance(dso, SimpleDSpaceObject): - logging.error(f'Only SimpleDSpaceObject types (eg Item, Collection, Community) ' + _logger.error(f'Only SimpleDSpaceObject types (eg Item, Collection, Community) ' f'are supported by generic update_dso PUT.') return dso try: @@ -489,14 +490,14 @@ def update_dso(self, dso, params=None): if r.status_code == 200: # 200 OK - success! updated_dso = dso_type(parse_json(r)) - logging.info(f'{updated_dso.type} {updated_dso.uuid} updated sucessfully!') + _logger.debug(f'{updated_dso.type} {updated_dso.uuid} updated sucessfully!') return updated_dso else: - logging.error(f'update operation failed: {r.status_code}: {r.text} ({url})') + _logger.error(f'update operation failed: {r.status_code}: {r.text} ({url})') return None except ValueError as e: - logging.error("Error parsing DSO response", exc_info=True) + _logger.error("Error parsing DSO response", exc_info=True) return None def delete_dso(self, dso=None, url=None, params=None): @@ -511,11 +512,11 @@ def delete_dso(self, dso=None, url=None, params=None): """ if dso is None: if url is None: - logging.error(f'Need a DSO or a URL to delete') + _logger.error(f'Need a DSO or a URL to delete') return None else: if not isinstance(dso, SimpleDSpaceObject): - logging.error(f'Only SimpleDSpaceObject types (eg Item, Collection, Community, EPerson) ' + _logger.error(f'Only SimpleDSpaceObject types (eg Item, Collection, Community, EPerson) ' f'are supported by generic update_dso PUT.') return dso # Get self URI from HAL links @@ -525,13 +526,13 @@ def delete_dso(self, dso=None, url=None, params=None): r = self.api_delete(url, params=params) if r.status_code == 204: # 204 No Content - success! - logging.info(f'{url} was deleted sucessfully!') + _logger.info(f'{url} was deleted sucessfully!') return r else: - logging.error(f'update operation failed: {r.status_code}: {r.text} ({url})') + _logger.error(f'update operation failed: {r.status_code}: {r.text} ({url})') return None except ValueError as e: - logging.error(f'Error deleting DSO {dso.uuid}: {e}') + _logger.error(f'Error deleting DSO {dso.uuid}: {e}') return None # PAGINATION @@ -570,7 +571,7 @@ def get_bundles(self, parent=None, uuid=None, page=0, size=20, sort=None): for resource in resources: bundles.append(Bundle(resource)) except ValueError as err: - logging.error(f'error parsing bundle results: {err}') + _logger.error(f'error parsing bundle results: {err}') return bundles @@ -607,7 +608,7 @@ def get_bitstreams(self, uuid=None, bundle=None, page=0, size=20, sort=None): url = bundle.links['bitstreams']['href'] else: url = f'{self.API_ENDPOINT}/core/bundles/{bundle.uuid}/bitstreams' - logging.warning(f'Cannot find bundle bitstream links, will try to construct manually: {url}') + _logger.warning(f'Cannot find bundle bitstream links, will try to construct manually: {url}') # Perform the actual request. By now, our URL and parameter should be properly set params = {} if size is not None: @@ -658,23 +659,23 @@ def create_bitstream(self, bundle=None, name=None, path=None, mime=None, metadat r = self.session.send(prepared_req) if 'DSPACE-XSRF-TOKEN' in r.headers: t = r.headers['DSPACE-XSRF-TOKEN'] - logging.debug('Updating token to ' + t) + _logger.debug('Updating token to ' + t) self.session.headers.update({'X-XSRF-Token': t}) self.session.cookies.update({'X-XSRF-Token': t}) if r.status_code == 403: r_json = parse_json(r) if 'message' in r_json and 'CSRF token' in r_json['message']: if retry: - logging.error('Already retried... something must be wrong') + _logger.error('Already retried... something must be wrong') else: - logging.debug("Retrying request with updated CSRF token") + _logger.debug("Retrying request with updated CSRF token") return self.create_bitstream(bundle, name, path, mime, metadata, True) if r.status_code == 201 or r.status_code == 200: # Success return Bitstream(api_resource=parse_json(r)) else: - logging.error(f'Error creating bitstream: {r.status_code}: {r.text}') + _logger.error(f'Error creating bitstream: {r.status_code}: {r.text}') return None def download_bitstream(self, uuid=None): @@ -715,14 +716,14 @@ def get_communities(self, uuid=None, page=0, size=20, sort=None, top=False): url = f'{url}/{uuid}' params = None except ValueError: - logging.error(f'Invalid community UUID: {uuid}') + _logger.error(f'Invalid community UUID: {uuid}') return None if top: # Set new URL url = f'{url}/search/top' - logging.debug(f'Performing get on {url}') + _logger.debug(f'Performing get on {url}') # Perform actual get r_json = self.fetch_resource(url, params) # Empty list @@ -778,7 +779,7 @@ def get_collections(self, uuid=None, community=None, page=0, size=20, sort=None) url = f'{url}/{uuid}' params = None except ValueError: - logging.error(f'Invalid collection UUID: {uuid}') + _logger.error(f'Invalid collection UUID: {uuid}') return None if community is not None: @@ -845,7 +846,7 @@ def get_item(self, uuid): url = f'{url}/{uuid}' return self.api_get(url, None, None) except ValueError: - logging.error(f'Invalid item UUID: {uuid}') + _logger.error(f'Invalid item UUID: {uuid}') return None def get_items(self): @@ -882,11 +883,11 @@ def create_item(self, parent, item): """ url = f'{self.API_ENDPOINT}/core/items' if parent is None: - logging.error('Need a parent UUID!') + _logger.error('Need a parent UUID!') return None params = {'owningCollection': parent} if not isinstance(item, Item): - logging.error('Need a valid item') + _logger.error('Need a valid item') return None return Item(api_resource=parse_json(self.create_dso(url, params=params, data=item.as_dict()))) @@ -898,7 +899,7 @@ def update_item(self, item): @return: """ if not isinstance(item, Item): - logging.error('Need a valid item') + _logger.error('Need a valid item') return None return self.update_dso(item, params=None) @@ -916,7 +917,7 @@ def add_metadata(self, dso, field, value, language=None, authority=None, confide """ if dso is None or field is None or value 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') + _logger.error('Invalid or missing DSpace object, field or value string') return self dso_type = type(dso) @@ -957,7 +958,7 @@ def create_user(self, user, token=None): def delete_user(self, user): if not isinstance(user, User): - logging.error(f'Must be a valid user') + _logger.error(f'Must be a valid user') return None return self.delete_dso(user) @@ -997,7 +998,7 @@ def create_group(self, group): 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)) - logging.debug(res) + _logger.debug(res) # TODO: WIP def update_token(self, r): @@ -1009,11 +1010,11 @@ def update_token(self, r): :return: """ if not self.session: - logging.debug('Session state not found, setting...') + _logger.debug('Session state not found, setting...') self.session = requests.Session() if 'DSPACE-XSRF-TOKEN' in r.headers: t = r.headers['DSPACE-XSRF-TOKEN'] - logging.debug(f'Updating XSRF token to {t}') + _logger.debug(f'Updating XSRF token to {t}') # Update headers and cookies self.session.headers.update({'X-XSRF-Token': t}) self.session.cookies.update({'X-XSRF-Token': t}) @@ -1024,7 +1025,7 @@ def get_short_lived_token(self): @return: short lived Authorization token """ if not self.session: - logging.debug('Session state not found, setting...') + _logger.debug('Session state not found, setting...') self.session = requests.Session() url = f'{self.API_ENDPOINT}/authn/shortlivedtokens' @@ -1033,7 +1034,7 @@ def get_short_lived_token(self): if r_json is not None and 'token' in r_json: return r_json['token'] - logging.error('Could not retrieve short-lived token') + _logger.error('Could not retrieve short-lived token') return None def solr_query(self, query, filters=None, fields=None, start=0, rows=999999999): From dc698dd95e400a8e88de1ae9b6112cb4d698fb4c Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Mon, 28 Oct 2024 22:53:58 +0100 Subject: [PATCH 08/25] 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 09/25] 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 10/25] 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 11/25] 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 12/25] 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 13/25] 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 14/25] 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 15/25] 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 16/25] 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 17/25] 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 18/25] 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 19/25] 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 20/25] 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 21/25] 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 22/25] 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 23/25] 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 24/25] 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 25/25] 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: