-
Notifications
You must be signed in to change notification settings - Fork 26
Feature/extend item crud operations #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a238eb5
2f99ee1
0d644df
dcf8b21
a4e2c79
72c29da
c2e3b3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixes #29 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh: this is a breaking API change so we should bump to version 0.2 after merging this |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given all this does is one line, I'm wondering what the advantage of this function is... the auto exception handling? My thinking when using the constructors like this is that it lets us be a bit more abstract, and do things like
Does that make sense? Do you think we need to just be more strict about handling each different DSO type? Or, if the error handling is the point, could we instead change this to _construct_dso(self, dso_data, dso_type) ? |
||
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}" | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = "[email protected]" | ||
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kshepherd How much do we care about having a clean
git blame
?I would strongly prefer at the very least that mere formatting changes like this be in their own commit. (I assume this is a
black
thing?)