Skip to content

Commit

Permalink
Update model handling of API calls
Browse files Browse the repository at this point in the history
Why these changes are being introduced:
Having the model API call methods return the request response ended up
being cumbersome for the application using this library. Although this
is more prescriptive, having the methods update the model instances in
response to the API calls simplifies workflows elsewhere.

How this addresses that need:
* Updates the Bitstream and Item classes to have post and delete
  methods update instance attributes rather than returning the response
  object
* Updates relevant tests
* Minor corrections in client and utils modules

Relevant ticket(s):
* https://mitlibraries.atlassian.net/browse/ETD-398
  • Loading branch information
hakbailey committed Sep 13, 2021
1 parent feb8263 commit 57c8fba
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 88 deletions.
95 changes: 78 additions & 17 deletions dspace/bitstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from typing import Optional

import smart_open
from requests import Response

from dspace.client import DSpaceClient
from dspace.errors import MissingFilePathError
Expand All @@ -26,9 +25,26 @@ class Bitstream:
name: Name of the Bitstream
Attributes:
description (str): Description of the bitstream
file_path (str): File path to the bitstream. Required to post bitstream to DSpace
name (str): Name of the Bitstream
bundleName(Optional[str]): The name of the DSpace bundle of the bitstream
checkSum (Optional[dict[str:str]]): The DSpace-calculated checksum for the
bitstream, e.g. {"value":"62778292a3a6dccbe2662a2bfca3b86e",
"checkSumAlgorithm":"MD5"}
description (Optional[str]): Description of the bitstream
format (Optional[str]): The DSpace-identified file format of the bitstream
expand (List[str]: The expand options for the DSpace REST object
file_path (Optional[str]): File path to the bitstream. Required to post
bitstream to DSpace
link (Optional[str]): The DSpace REST API path for the bitstream
mimeType (Optional[str]): The DSpace-identified mimetype of the bitstream
name (Optional[str]): The name of the bitstream in DSpace
parentObject (Optional[str]): Parent object of the bitstream in DSpace
policies (Optional[list]): Resource policies applied to the bitstream
retrieveLink (Optional[str]): The DSpace REST API path to directly retrieve the
bitstream file
sequenceId (Optional[int]): The DSpace bitstream sequence ID
sizeBytes (Optional[int]): The DSpace-identified size of the bitstream in bytes
type (Optional[str]): The DSpace object type
uuid (Optional[str]): UUID of the bitstream in DSpace
"""

def __init__(
Expand All @@ -41,48 +57,76 @@ def __init__(
self.file_path = file_path
self.name = name

self.bundleName = None
self.checkSum = None
self.format = None
self.expand = ["parent", "policies", "all"]
self.link = None
self.mimeType = None
self.parentObject = None
self.policies = None
self.retrieveLink = None
self.sequenceId = None
self.sizeBytes = None
self.type = "bitstream"
self.uuid = None

def delete(
self,
client: DSpaceClient,
bitstream_uuid: str,
) -> Response:
"""Delete bitstream and return the response.
) -> None:
"""Delete bitstream from DSpace and unset relevant bitstream attributes.
Args:
client: An authenticated instance of the :class:`DSpaceClient` class
Returns:
:class:`requests.Response` object
Raises:
:class:`requests.HTTPError`: 404 Not Found if no bitstream matching
provided UUID
"""
return client.delete(f"/bitstreams/{bitstream_uuid}")
logger.debug(
"Deleting bitstream with uuid %s from %s", self.uuid, client.base_url
)
response = client.delete(f"/bitstreams/{self.uuid}")
logger.debug("Delete response: %s", response)
self.bundleName = None
self.checkSum = None
self.format = None
self.link = None
self.mimeType = None
self.parentObject = None
self.policies = None
self.retrieveLink = None
self.sequenceId = None
self.sizeBytes = None
self.uuid = None

def post(
self,
client: DSpaceClient,
item_handle: Optional[str] = None,
item_uuid: Optional[str] = None,
) -> Response:
"""Post bitstream to an item and return the response.
) -> None:
"""Post bitstream to an item and set bitstream attributes to response object values.
Requires either the `item_handle` or the `item_uuid`, but not both. If
both are passed, defaults to using the UUID.
The `smart_open library <https://pypi.org/project/smart-open/>`_ replaces the
standard open function to stream local or remote files for the POST request.
Note: DSpace internally uses the file extension from the provided "name"
parameter (the `bitstream.name` attribute) to assign a format and mimeType when
creating a new bitstream via API post. If a name with a file extension (e.g.,
"test-file.pdf") is not provided, DSpace will assign a format of "Unknown" and
a mimeType of "application/octet-stream".
Args:
client: An authenticated instance of the :class:`DSpaceClient` class
item_handle: The handle of an existing item in DSpace to post the bitstream
to
item_uuid: The UUID of an existing item in DSpace to post the bitstream to
Returns:
:class:`requests.Response` object
Raises:
:class:`requests.HTTPError`: 404 Not Found if no item matching
provided handle/UUID
Expand All @@ -97,4 +141,21 @@ def post(
endpoint = f"/items/{item_id}/bitstreams"
params = {"name": self.name, "description": self.description}
data = smart_open.open(self.file_path, "rb")
return client.post(endpoint, data=data, params=params)
logger.debug(
"Posting new bitstream to %s with info %s",
client.base_url + endpoint,
params,
)
response = client.post(endpoint, data=data, params=params).json()
logger.debug("Post response: %s", response)
self.bundleName = response["bundleName"]
self.checkSum = response["checkSum"]
self.format = response["format"]
self.link = response["link"]
self.mimeType = response["mimeType"]
self.parentObject = response["parentObject"]
self.policies = response["policies"]
self.retrieveLink = response["retrieveLink"]
self.sequenceId = response["sequenceId"]
self.sizeBytes = response["sizeBytes"]
self.uuid = response["uuid"]
5 changes: 3 additions & 2 deletions dspace/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ def delete(self, endpoint: str) -> requests.Response:
DELETE requests using the client's stored authentication cookie and headers.
Args:
endpoint: The DSPace REST endpoint for object to delete, e.g. "/items/abc123"
endpoint: The DSPace REST endpoint for the object to delete, e.g.
"/items/7c8e7bbc-e36b-4194-87e5-5347e3a69a57"
Returns:
:class:`requests.Response` object
Expand All @@ -61,7 +62,7 @@ def delete(self, endpoint: str) -> requests.Response:
"""
url = self.base_url + endpoint
response = requests.delete(
url, cookies=self.cookies, headers=self.headers, timeout=5.0
url, cookies=self.cookies, headers=self.headers, timeout=15.0
)
response.raise_for_status()
return response
Expand Down
94 changes: 70 additions & 24 deletions dspace/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import logging
from typing import Dict, List, Optional

from requests import Response

from dspace.bitstream import Bitstream
from dspace.client import DSpaceClient
from dspace.utils import select_identifier
Expand All @@ -27,10 +25,24 @@ class Item:
metadata: :class:`MetadataEntry` objects to associate with the item
Attributes:
bitstreams (list): List of :class:`Bitstream` objects belonging to the item
metadata (list of :obj:`MetadataEntry`): List of
archived (Optional[str]): Item archived status in DSpace ("true" or "false")
bitstreams (Optional[List[:obj:`Bitstream`]]): List of :class:`Bitstream`
objects belonging to the item
expand (List[str]): The expand options for the DSpace REST object
handle (Optional[str]): The handle of the item in DSpace
lastModified (Optional[str]): Timestamp the item was last modified in DSpace
link (Optional[str]): The DSpace REST API path for the item
metadata (Optional[List:obj:`MetadataEntry`]]): List of
:class:`MetadataEntry` objects representing the item's metadata
name (Optional[str]): The name of the item in DSpace
parentCollection (Optional[str]): Parent collection of the item in DSpace
parentCollectionList (Optiona[List[str]]): List of parent collections of the
item in DSpace
parentCommunityList (Optional[List[str]]): List of parent communities of the
item in DSpace
type (str): The DSpace object type
uuid (Optional[str]): The internal UUID of the item in DSpace
withdrawn (Optional[str]): Item withdrawn status in DSpace ("true" or "false")
"""

def __init__(
Expand All @@ -41,32 +53,57 @@ def __init__(
self.bitstreams = bitstreams or []
self.metadata = metadata or []

def delete(
self,
client: DSpaceClient,
item_uuid: str,
) -> Response:
"""Delete item and return the response.
self.archived = None
self.expand = [
"metadata",
"parentCollection",
"parentCollectionList",
"parentCommunityList",
"bitstreams",
"all",
]
self.handle = None
self.lastModified = None
self.link = None
self.name = None
self.parentCollection = None
self.parentCollectionList = None
self.parentCommunityList = None
self.type = "item"
self.uuid = None
self.withdrawn = None

def delete(self, client: DSpaceClient) -> None:
"""Delete item from DSpace and unset relevant item attributes.
Args:
client: An authenticated instance of the :class:`DSpaceClient` class
Returns:
:class:`requests.Response` object
Raises:
:class:`requests.HTTPError`: 404 Not Found if no item matching
provided UUID
"""
return client.delete(f"/items/{item_uuid}")
logger.debug("Deleting item with uuid %s from %s", self.uuid, client.base_url)
response = client.delete(f"/items/{self.uuid}")
logger.debug("Delete response: %s", response)
self.archived = None
self.handle = None
self.lastModified = None
self.link = None
self.name = None
self.parentCollection = None
self.parentCollectionList = None
self.parentCommunityList = None
self.uuid = None
self.withdrawn = None

def post(
self,
client: DSpaceClient,
collection_handle: Optional[str] = None,
collection_uuid: Optional[str] = None,
) -> Response:
"""Post item to a collection and return the response.
) -> None:
"""Post item to a collection and set item attributes to response object values.
Requires either the `collection_handle` or the `collection_uuid`, but not both.
If both are passed, defaults to using the UUID.
Expand All @@ -78,9 +115,6 @@ def post(
collection_uuid: The UUID of an existing collection in DSpace to post the
item to
Returns:
:class:`requests.Response` object
Raises:
:class:`requests.HTTPError`: 404 Not Found if no collection matching
provided handle/UUID
Expand All @@ -91,10 +125,22 @@ def post(
endpoint = f"/collections/{collection_id}/items"
metadata = {"metadata": [m.to_dict() for m in self.metadata]}
logger.debug(
f"Posting new item to {client.base_url}{endpoint} with metadata "
f"{metadata}"
"Posting new item to %s with metadata %s",
client.base_url + endpoint,
metadata,
)
return client.post(endpoint, json=metadata)
response = client.post(endpoint, json=metadata).json()
logger.debug("Post response: %s", response)
self.archived = response["archived"]
self.handle = response["handle"]
self.lastModified = response["lastModified"]
self.link = response["link"]
self.name = response["name"]
self.parentCollection = response["parentCollection"]
self.parentCollectionList = response["parentCollectionList"]
self.parentCommunityList = response["parentCommunityList"]
self.uuid = response["uuid"]
self.withdrawn = response["withdrawn"]


class MetadataEntry:
Expand All @@ -112,7 +158,7 @@ class MetadataEntry:
Attributes:
key (str): Name of the metadata entry
value (str): Value of the metadata entry
language (str, optional): Language of the metadata entry
language (Optional[str]): Language of the metadata entry
"""

def __init__(self, key: str, value: str, language: Optional[str] = None):
Expand Down
2 changes: 1 addition & 1 deletion dspace/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
def select_identifier(
client: DSpaceClient, handle: Optional[str], uuid: Optional[str]
) -> str:
"""Return the uuid of an item given a handle, a uuid, or both.
"""Return the uuid of a DSpace object given a handle, a uuid, or both.
Args:
client: Authenticated instance of :class:`DSpaceClient` class
Expand Down
Loading

0 comments on commit 57c8fba

Please sign in to comment.