diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 44db8c3..fafc1ad 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}) @@ -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): """ @@ -1255,6 +1284,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, + 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 isinstance(item, Item): + logging.error("Need a valid item") + 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, @@ -1311,6 +1420,22 @@ def add_metadata( return dso_type(api_resource=parse_json(r)) + 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: + 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) + return None + def create_user(self, user, token=None, embeds=None): """ Create a user @@ -1468,13 +1593,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}" + ) 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")