From 2f99ee1fa9165800dd2c20094bb3275cf69b2cf6 Mon Sep 17 00:00:00 2001 From: Stefan Szepe Date: Thu, 2 Jan 2025 16:28:32 +0100 Subject: [PATCH 1/6] add patch_item function --- dspace_rest_client/client.py | 98 +++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 8 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 44db8c3..7ce8b0e 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -487,7 +487,7 @@ def search_objects( size=20, sort=None, dso_type=None, - configuration='default', + configuration="default", embeds=None, ): """ @@ -521,7 +521,7 @@ def search_objects( if sort is not None: params["sort"] = sort if configuration is not None: - params['configuration'] = configuration + params["configuration"] = configuration r_json = self.fetch_resource(url=url, params={**params, **filters}) @@ -552,7 +552,7 @@ def search_objects_iter( filters=None, dso_type=None, sort=None, - configuration='default', + configuration="default", embeds=None, ): """ @@ -579,7 +579,7 @@ def search_objects_iter( if sort is not None: params["sort"] = sort if configuration is not None: - params['configuration'] = configuration + params["configuration"] = configuration return do_paginate(url, {**params, **filters}) @@ -1255,6 +1255,86 @@ def update_item(self, item, embeds=None): return None return self.update_dso(item, params=parse_params(embeds=embeds)) + def patch_item( + self, + item_uuid, + operation, + field, + value=None, + language=None, + authority=None, + confidence=-1, + place="", + ): + """ + Patch item. This method performs a partial update operation (PATCH) on the given Item object. + Supports operations: 'add', 'remove', 'replace'. Does not support 'move'. + + @param item: Python Item object containing all the necessary data, identifiers, and links. + @param operation: Operation to perform ('add', 'remove', 'replace'). + @param path: Path to the field or property to patch. + @param value: New value for the specified path (required for 'add' and 'replace'). Ignored for 'remove'. + @return: The API response or None in case of an error. + """ + try: + if not item_uuid: + logging.error("Item UUID is required") + return None + + if not field or not value: + logging.error("Field and value are required") + return None + + if not operation or operation not in [ + self.PatchOperation.ADD, + self.PatchOperation.REPLACE, + self.PatchOperation.REMOVE, + ]: + logging.error("Unsupported operation: %s", operation) + return None + + if ( + operation in [self.PatchOperation.ADD, self.PatchOperation.REPLACE] + and value is None + ): + logging.error("Value is required for 'add' and 'replace' operations") + return None + + # Construct the item URI + item_uri = f"{self.API_ENDPOINT}/core/items/{item_uuid}" + + path = f"/metadata/{field}/{place}" + patch_value = { + "value": value, + "language": language, + "authority": authority, + "confidence": confidence, + } + + # Perform the patch operation + response = self.api_patch( + url=item_uri, + operation=operation, + path=path, + value=patch_value, + ) + + if response.status_code in [200, 204]: + logging.info("Successfully patched item: %s", item_uuid) + return response + else: + logging.error( + "Failed to patch item: %s (Status: %s, Response: %s)", + item_uuid, + response.status_code, + response.text, + ) + return None + + except ValueError: + logging.error("Error processing patch operation", exc_info=True) + return None + def add_metadata( self, dso, @@ -1468,13 +1548,15 @@ def resolve_identifier_to_dso(self, identifier=None): @return: resolved DSpaceObject or error """ if identifier is not None: - url = f'{self.API_ENDPOINT}/pid/find' - r = self.api_get(url, params={'id': identifier}) + url = f"{self.API_ENDPOINT}/pid/find" + r = self.api_get(url, params={"id": identifier}) if r.status_code == 200: r_json = parse_json(r) - if r_json is not None and 'uuid' in r_json: + if r_json is not None and "uuid" in r_json: return DSpaceObject(api_resource=r_json) elif r.status_code == 404: logging.error(f"Not found: {identifier}") else: - logging.error(f"Error resolving identifier {identifier} to DSO: {r.status_code}") + logging.error( + f"Error resolving identifier {identifier} to DSO: {r.status_code}" + ) From 0d644df67596c8773a4e062b835776783fd670ac Mon Sep 17 00:00:00 2001 From: Stefan Szepe Date: Thu, 2 Jan 2025 16:33:53 +0100 Subject: [PATCH 2/6] add delete_item function --- dspace_rest_client/client.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 7ce8b0e..684bc4a 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1280,11 +1280,11 @@ def patch_item( if not item_uuid: logging.error("Item UUID is required") return None - + if not field or not value: logging.error("Field and value are required") return None - + if not operation or operation not in [ self.PatchOperation.ADD, self.PatchOperation.REPLACE, @@ -1391,6 +1391,19 @@ def add_metadata( return dso_type(api_resource=parse_json(r)) + def delete_item(self, item_uuid): + """ + Delete an item, given its UUID + @param item_uuid: the UUID of the item + @return: the raw API response + """ + try: + url = f"{self.API_ENDPOINT}/core/items/{item_uuid}" + return self.api_delete(url) + except ValueError: + logging.error("Invalid item UUID: %s", item_uuid) + return None + def create_user(self, user, token=None, embeds=None): """ Create a user From dcf8b210aaa0f034d809af822dac583197e794a2 Mon Sep 17 00:00:00 2001 From: Stefan Szepe Date: Thu, 2 Jan 2025 16:46:19 +0100 Subject: [PATCH 3/6] construct Item object as a return of get_item function --- dspace_rest_client/client.py | 39 +++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 684bc4a..90736c9 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1149,18 +1149,47 @@ def get_item(self, uuid, embeds=None): """ Get an item, given its UUID @param uuid: the UUID of the item - @param embeds: Optional list of resources to embed in response JSON - @return: the raw API response + @param embeds: Optional list of resources to embed in response JSON + @return: the constructed Item object or None if an error occurs """ - # TODO - return constructed Item object instead, handling errors here? url = f"{self.API_ENDPOINT}/core/items" try: - id = UUID(uuid).version + # Validate the UUID format + id_version = UUID(uuid).version url = f"{url}/{uuid}" - return self.api_get(url, parse_params(embeds=embeds), None) + + # Make API GET request + response = self.api_get(url, parse_params(embeds=embeds), None) + + # Handle successful response + if response.status_code == 200: + # Parse the response JSON into an Item object + return self._construct_item(response.json()) + else: + logging.error( + "Failed to retrieve item. Status code: %s", response.status_code + ) + logging.error("Response: %s", response.text) + return None except ValueError: logging.error("Invalid item UUID: %s", uuid) return None + except Exception as e: + logging.error("An unexpected error occurred: %s", str(e)) + return None + + def _construct_item(self, item_data): + """ + Construct an Item object from API response data + @param item_data: The raw JSON data from the API + @return: An Item object + """ + try: + # Create an Item instance, using the API response data + return Item(api_resource=item_data) + except KeyError as e: + logging.error("Missing expected key in item data: %s", str(e)) + return None def get_items(self, embeds=None): """ From a4e2c797dc6c30f02b367571e08557b0e32fc402 Mon Sep 17 00:00:00 2001 From: Stefan Szepe Date: Thu, 2 Jan 2025 16:54:20 +0100 Subject: [PATCH 4/6] update patch_item function to use Item instead of item_uuid --- dspace_rest_client/client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 90736c9..4e7f7e3 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1286,7 +1286,7 @@ def update_item(self, item, embeds=None): def patch_item( self, - item_uuid, + item, operation, field, value=None, @@ -1306,8 +1306,8 @@ def patch_item( @return: The API response or None in case of an error. """ try: - if not item_uuid: - logging.error("Item UUID is required") + if not isinstance(item, Item): + logging.error("Need a valid item") return None if not field or not value: @@ -1330,7 +1330,7 @@ def patch_item( return None # Construct the item URI - item_uri = f"{self.API_ENDPOINT}/core/items/{item_uuid}" + item_uri = f"{self.API_ENDPOINT}/core/items/{item.uuid}" path = f"/metadata/{field}/{place}" patch_value = { @@ -1349,12 +1349,12 @@ def patch_item( ) if response.status_code in [200, 204]: - logging.info("Successfully patched item: %s", item_uuid) + logging.info("Successfully patched item: %s", item.uuid) return response else: logging.error( "Failed to patch item: %s (Status: %s, Response: %s)", - item_uuid, + item.uuid, response.status_code, response.text, ) From 72c29da2ba6a5637b66154f4886907e63465bc9d Mon Sep 17 00:00:00 2001 From: Stefan Szepe Date: Thu, 2 Jan 2025 16:55:54 +0100 Subject: [PATCH 5/6] add example patch item script --- example_patch.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 example_patch.py diff --git a/example_patch.py b/example_patch.py new file mode 100644 index 0000000..21a848c --- /dev/null +++ b/example_patch.py @@ -0,0 +1,85 @@ +# This software is licenced under the BSD 3-Clause licence +# available at https://opensource.org/licenses/BSD-3-Clause +# and described in the LICENCE file in the root of this project + +""" +Example Python 3 application using the dspace.py API client library to patch +some resources in a DSpace 7 repository. +""" +from pprint import pprint + +import os +import sys + +from dspace_rest_client.client import DSpaceClient +from dspace_rest_client.models import Community, Collection, Item, Bundle, Bitstream + +DEFAULT_URL = "https://localhost:8080/server/api" +DEFAULT_USERNAME = "username@test.system.edu" +DEFAULT_PASSWORD = "password" + +# UUIDs for the object we want to patch +RESOURCE_ID = "0128787c-6f79-4661-aea4-11635d6fb04f" + +# Field and value to patch +FIELD = "dc.title" +VALUE = "New title" + +# Configuration from environment variables +URL = os.environ.get("DSPACE_API_ENDPOINT", DEFAULT_URL) +USERNAME = os.environ.get("DSPACE_API_USERNAME", DEFAULT_USERNAME) +PASSWORD = os.environ.get("DSPACE_API_PASSWORD", DEFAULT_PASSWORD) + +# Instantiate DSpace client +d = DSpaceClient( + api_endpoint=URL, username=USERNAME, password=PASSWORD, fake_user_agent=True +) + +# Authenticate against the DSpace client +authenticated = d.authenticate() +if not authenticated: + print("Error logging in! Giving up.") + sys.exit(1) + +# An example of searching for workflow items (any search configuration from discovery.xml can be used) +# note that the results here depend on the workflow role / access of the logged in user +search_results = d.search_objects( + query=f"search.resourceid:{RESOURCE_ID}", dso_type="item" +) +for result in search_results: + print(f"{result.name} ({result.uuid})") + print( + f"{FIELD}: {result.metadata.get(FIELD, [{'value': 'Not available'}])[0]['value']}" + ) + + item = d.get_item(uuid=result.uuid) + print(type(item)) + + if FIELD in result.metadata: + if result.metadata[FIELD][0]["value"] == VALUE: + print("Metadata is already correct, skipping") + continue + elif result.metadata[FIELD][0]["value"] != VALUE: + patch_op = d.patch_item( + item=item, + operation="replace", + field=FIELD, + value=VALUE, + ) + if patch_op: + print(patch_op) + print("Metadata updated") + else: + print("Error updating metadata") + else: + patch_op = d.patch_item( + item=item, + operation="add", + field=FIELD, + value=VALUE, + ) + if patch_op: + print(patch_op) + print("Metadata added") + else: + print("Error adding metadata") From c2e3b3b5f0ffaca860c77c2fdb18d6573f8dfd49 Mon Sep 17 00:00:00 2001 From: Stefan Szepe Date: Thu, 2 Jan 2025 17:41:03 +0100 Subject: [PATCH 6/6] update delete_item function to use Item instead of item_uuid --- dspace_rest_client/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 4e7f7e3..fafc1ad 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -1420,17 +1420,20 @@ def add_metadata( return dso_type(api_resource=parse_json(r)) - def delete_item(self, item_uuid): + def delete_item(self, item): """ Delete an item, given its UUID @param item_uuid: the UUID of the item @return: the raw API response """ try: - url = f"{self.API_ENDPOINT}/core/items/{item_uuid}" + if not isinstance(item, Item): + logging.error("Need a valid item") + return None + url = f"{self.API_ENDPOINT}/core/items/{item.uuid}" return self.api_delete(url) except ValueError: - logging.error("Invalid item UUID: %s", item_uuid) + logging.error("Invalid item UUID: %s", item.uuid) return None def create_user(self, user, token=None, embeds=None):