-
Notifications
You must be signed in to change notification settings - Fork 0
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
Implement endpoint for deleting attachments by entity ID #105
base: develop
Are you sure you want to change the base?
Changes from all commits
1cac970
f3cf2eb
dc1143a
6679b73
af93018
6023fab
3170918
20329b5
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 |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
|
||
from fastapi import APIRouter, Depends, Path, Query, status | ||
|
||
from object_storage_api.core.exceptions import InvalidObjectIdError | ||
from object_storage_api.schemas.attachment import ( | ||
AttachmentMetadataSchema, | ||
AttachmentPostResponseSchema, | ||
|
@@ -82,3 +83,23 @@ def delete_attachment( | |
# pylint: disable=missing-function-docstring | ||
logger.info("Deleting attachment with ID: %s", attachment_id) | ||
attachment_service.delete(attachment_id) | ||
|
||
|
||
@router.delete( | ||
path="", | ||
summary="Delete attachments by entity ID", | ||
response_description="Attachments deleted successfully", | ||
status_code=status.HTTP_204_NO_CONTENT, | ||
) | ||
def delete_attachments_by_entity_id( | ||
entity_id: Annotated[str, Query(description="The entity ID of the attachments to delete")], | ||
attachment_service: AttachmentServiceDep, | ||
) -> None: | ||
# pylint: disable=missing-function-docstring | ||
logger.info("Deleting attachments with entity ID: %s", entity_id) | ||
try: | ||
attachment_service.delete_by_entity_id(entity_id) | ||
except InvalidObjectIdError: | ||
# As this endpoint takes in a query parameter to delete multiple attachments, and to hide the database | ||
# behaviour, we treat any invalid entity_id the same as a valid one that has no attachments associated to it. | ||
pass | ||
Comment on lines
+100
to
+105
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. Whilst it is fine to catch the exception in the router layer, I think it would be better if it was in the repo layer to match what we are doing in the rest of the repo. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -88,3 +88,22 @@ def delete(self, object_key: str) -> None: | |||||
Bucket=object_storage_config.bucket_name.get_secret_value(), | ||||||
Key=object_key, | ||||||
) | ||||||
|
||||||
def delete_many(self, object_keys: list[str]) -> None: | ||||||
""" | ||||||
Deletes given attachments from object storage by object keys. | ||||||
|
||||||
It does this in batches due to the `delete_objects` request only allowing a list of up to 1000 keys. | ||||||
|
||||||
:param object_keys: Keys of the attachments to delete. | ||||||
""" | ||||||
logger.info("Deleting attachment files with object keys: %s from the object store", object_keys) | ||||||
|
||||||
batch_size = 1000 | ||||||
# Loop through the list of object keys in steps of `batch_size` | ||||||
for i in range(0, len(object_keys), batch_size): | ||||||
batch = object_keys[i : i + batch_size] | ||||||
s3_client.delete_objects( | ||||||
Bucket=object_storage_config.bucket_name.get_secret_value(), | ||||||
Delete={"Objects": [{"Key": key for key in batch}]}, | ||||||
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.
Suggested change
The current implementation works, but I'm not sure why it does? The docs for the function requires the parameter to be structured as so: If I modify the code to compare the 2 outputs of both options this is the result. Again both implementation work, but I don't understand why yours works? |
||||||
) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -360,3 +360,48 @@ def test_delete_with_invalid_id(self): | |||||
"""Test deleting an attachment with an invalid ID.""" | ||||||
self.delete_attachment("invalid_id") | ||||||
self.check_delete_attachment_failed_with_detail(404, "Attachment not found") | ||||||
|
||||||
|
||||||
class DeleteByEntityIdDSL(ListDSL): | ||||||
"""Base class for delete tests.""" | ||||||
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.
Suggested change
|
||||||
|
||||||
_delete_response_attachments: Response | ||||||
|
||||||
def delete_attachments_by_entity_id(self, entity_id: str) -> None: | ||||||
""" | ||||||
Deletes attachments with the given entity ID. | ||||||
|
||||||
:param entity_id: Entity ID of the attachments to be deleted. | ||||||
""" | ||||||
self._delete_response_attachments = self.test_client.delete("/attachments", params={"entity_id": entity_id}) | ||||||
|
||||||
def check_delete_attachments_by_entity_id_success(self) -> None: | ||||||
""" | ||||||
Checks that a prior call to `delete_attachments_by_entity_id` gave a successful response with the expected code. | ||||||
""" | ||||||
assert self._delete_response_attachments.status_code == 204 | ||||||
|
||||||
|
||||||
class TestDeleteByEntityId(DeleteByEntityIdDSL): | ||||||
"""Tests for deleting attachments by entity ID.""" | ||||||
|
||||||
def test_delete_attachments_by_entity_id(self): | ||||||
"""Test deleting attachments.""" | ||||||
attachments = self.post_test_attachments() | ||||||
entity_id = attachments[0]["entity_id"] | ||||||
|
||||||
self.delete_attachments_by_entity_id(entity_id) | ||||||
self.check_delete_attachments_by_entity_id_success() | ||||||
|
||||||
self.get_attachments(filters={"entity_id": entity_id}) | ||||||
self.check_get_attachments_success([]) | ||||||
|
||||||
def test_delete_attachments_by_entity_id_with_non_existent_id(self): | ||||||
"""Test deleting attachments with a non-existent entity ID.""" | ||||||
self.delete_attachments_by_entity_id(str(ObjectId())) | ||||||
self.check_delete_attachments_by_entity_id_success() | ||||||
|
||||||
def test_delete_attachments_by_entity_id_with_invalid_id(self): | ||||||
Comment on lines
+388
to
+404
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. is |
||||||
"""Test deleting attachments with an invalid entity ID.""" | ||||||
self.delete_attachments_by_entity_id("invalid_id") | ||||||
self.check_delete_attachments_by_entity_id_success() |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -91,3 +91,17 @@ def mock_delete_one(collection_mock: Mock, deleted_count: int) -> None: | |||||
delete_result_mock = Mock(DeleteResult) | ||||||
delete_result_mock.deleted_count = deleted_count | ||||||
collection_mock.delete_one.return_value = delete_result_mock | ||||||
|
||||||
@staticmethod | ||||||
def mock_delete_many(collection_mock: Mock, deleted_count: int) -> None: | ||||||
""" | ||||||
Mock the `delete_many` method of the MongoDB database collection mock to return a `DeleteResult` object. The | ||||||
passed `deleted_count` value is returned as the `deleted_count` attribute of the `DeleteResult` object, enabling | ||||||
for the code that relies on the `deleted_count` value to work. | ||||||
|
||||||
:param collection_mock: Mocked MongoDB database collection instance. | ||||||
:param deleted_count: The value to be assigned to the `deleted_count` attribute of the `DeleteResult` object | ||||||
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.
Suggested change
|
||||||
""" | ||||||
delete_result_mock = Mock(DeleteResult) | ||||||
delete_result_mock.deleted_count = deleted_count | ||||||
collection_mock.delete_many.return_value = delete_result_mock |
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.