Merge pull request '[breaking] Clarify MinIO get() error semantics to match Redis behavior' (#33) from cursor/minio-error-semantics into main
Code Quality Pipeline / code-quality (push) Successful in 47s
Test Python Package / unit-tests (push) Successful in 1m17s
Test Python Package / coverage-report (push) Successful in 16s
Release on merge to main / release (push) Successful in 16s
Test Python Package / integration-tests (push) Successful in 1m6s
Code Quality Pipeline / code-quality (push) Successful in 47s
Test Python Package / unit-tests (push) Successful in 1m17s
Test Python Package / coverage-report (push) Successful in 16s
Release on merge to main / release (push) Successful in 16s
Test Python Package / integration-tests (push) Successful in 1m6s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/33
This commit was merged in pull request #33.
This commit is contained in:
@@ -172,15 +172,12 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
||||
self.logger.warning(
|
||||
f"Object '{object_name}' not found in bucket '{self._bucket_name}'"
|
||||
)
|
||||
else:
|
||||
self.logger.error(repr(exc))
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
self.logger.error(repr(exc))
|
||||
return None
|
||||
raise
|
||||
finally:
|
||||
if response is not None:
|
||||
response.close()
|
||||
response.release_conn()
|
||||
return None
|
||||
|
||||
def delete(self, object_name: str) -> None:
|
||||
"""Delete an object from the Minio bucket."""
|
||||
|
||||
@@ -9,7 +9,11 @@ class ObjectRepositoryInterface(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def get(self, object_name: str) -> BytesIO | None:
|
||||
"""Get an object by name."""
|
||||
"""Get an object by name.
|
||||
|
||||
Returns None when the object does not exist. Raises ConnectionError when
|
||||
not connected. Other backend errors propagate to the caller.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -218,10 +218,8 @@ def test_should_log_warning_when_getting_nonexistent_object(
|
||||
)
|
||||
|
||||
|
||||
def test_should_log_error_when_getting_with_s3error_other_than_no_such_key(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that the MinioAdapter logs an error for unhandled S3 errors."""
|
||||
def test_should_reraise_s3error_other_than_no_such_key() -> None:
|
||||
"""Test that the MinioAdapter re-raises unhandled S3 errors."""
|
||||
mock_client = MagicMock(spec=Minio)
|
||||
mock_client.bucket_exists.return_value = True
|
||||
other_s3error = S3Error(
|
||||
@@ -236,25 +234,20 @@ def test_should_log_error_when_getting_with_s3error_other_than_no_such_key(
|
||||
)
|
||||
mock_client.get_object.side_effect = other_s3error
|
||||
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
|
||||
with caplog.at_level("ERROR"):
|
||||
result = adapter.get("missing-object")
|
||||
assert result is None
|
||||
assert repr(other_s3error) in caplog.text
|
||||
with pytest.raises(S3Error) as exc_info:
|
||||
adapter.get("missing-object")
|
||||
assert exc_info.value.code == "UnhandledError"
|
||||
|
||||
|
||||
def test_should_log_error_when_getting_with_general_exception(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that the MinioAdapter logs an error on general exceptions during get."""
|
||||
def test_should_reraise_general_exception() -> None:
|
||||
"""Test that the MinioAdapter re-raises general exceptions during get."""
|
||||
mock_client = MagicMock(spec=Minio)
|
||||
mock_client.bucket_exists.return_value = True
|
||||
general_exception = Exception("General failure")
|
||||
mock_client.get_object.side_effect = general_exception
|
||||
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
|
||||
with caplog.at_level("ERROR"):
|
||||
result = adapter.get("missing-object")
|
||||
assert result is None
|
||||
assert repr(general_exception) in caplog.text
|
||||
with pytest.raises(Exception, match="General failure"):
|
||||
adapter.get("missing-object")
|
||||
|
||||
|
||||
def test_should_put_data(
|
||||
|
||||
@@ -5,7 +5,8 @@ from __future__ import annotations
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from minio import Minio
|
||||
from minio import Minio, S3Error
|
||||
from urllib3.response import BaseHTTPResponse
|
||||
|
||||
from python_repositories.adapters.minio_adapter import MinioAdapter
|
||||
from python_repositories.interfaces import ObjectRepositoryInterface
|
||||
@@ -119,8 +120,63 @@ def test_get_closes_response_when_read_fails() -> None:
|
||||
mock_client.get_object.return_value = mock_response
|
||||
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
|
||||
|
||||
result = adapter.get("some-object")
|
||||
with pytest.raises(OSError, match="connection reset"):
|
||||
adapter.get("some-object")
|
||||
|
||||
assert result is None
|
||||
mock_response.close.assert_called_once()
|
||||
mock_response.release_conn.assert_called_once()
|
||||
|
||||
|
||||
def test_get_returns_none_for_no_such_key() -> None:
|
||||
"""get() returns None when the object does not exist."""
|
||||
mock_client = MagicMock(spec=Minio)
|
||||
mock_client.bucket_exists.return_value = True
|
||||
mock_client.get_object.side_effect = S3Error(
|
||||
MagicMock(spec=BaseHTTPResponse),
|
||||
"NoSuchKey",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
bucket_name="test-bucket",
|
||||
object_name="missing-object",
|
||||
)
|
||||
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
|
||||
|
||||
result = adapter.get("missing-object")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_get_reraises_other_s3_errors() -> None:
|
||||
"""get() re-raises S3 errors other than NoSuchKey."""
|
||||
mock_client = MagicMock(spec=Minio)
|
||||
mock_client.bucket_exists.return_value = True
|
||||
other_s3error = S3Error(
|
||||
MagicMock(spec=BaseHTTPResponse),
|
||||
"AccessDenied",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
bucket_name="test-bucket",
|
||||
object_name="some-object",
|
||||
)
|
||||
mock_client.get_object.side_effect = other_s3error
|
||||
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
|
||||
|
||||
with pytest.raises(S3Error) as exc_info:
|
||||
adapter.get("some-object")
|
||||
|
||||
assert exc_info.value.code == "AccessDenied"
|
||||
|
||||
|
||||
def test_get_reraises_general_exception_from_get_object() -> None:
|
||||
"""get() re-raises unexpected exceptions from get_object."""
|
||||
mock_client = MagicMock(spec=Minio)
|
||||
mock_client.bucket_exists.return_value = True
|
||||
mock_client.get_object.side_effect = Exception("General failure")
|
||||
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
|
||||
|
||||
with pytest.raises(Exception, match="General failure"):
|
||||
adapter.get("some-object")
|
||||
|
||||
Reference in New Issue
Block a user