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

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:
2026-07-08 20:24:41 +02:00
4 changed files with 75 additions and 25 deletions
@@ -172,15 +172,12 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
self.logger.warning( self.logger.warning(
f"Object '{object_name}' not found in bucket '{self._bucket_name}'" f"Object '{object_name}' not found in bucket '{self._bucket_name}'"
) )
else: return None
self.logger.error(repr(exc)) raise
except Exception as exc: # pylint: disable=broad-except
self.logger.error(repr(exc))
finally: finally:
if response is not None: if response is not None:
response.close() response.close()
response.release_conn() response.release_conn()
return None
def delete(self, object_name: str) -> None: def delete(self, object_name: str) -> None:
"""Delete an object from the Minio bucket.""" """Delete an object from the Minio bucket."""
@@ -9,7 +9,11 @@ class ObjectRepositoryInterface(ABC):
@abstractmethod @abstractmethod
def get(self, object_name: str) -> BytesIO | None: 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 @abstractmethod
+9 -16
View File
@@ -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( def test_should_reraise_s3error_other_than_no_such_key() -> None:
caplog: pytest.LogCaptureFixture, """Test that the MinioAdapter re-raises unhandled S3 errors."""
) -> None:
"""Test that the MinioAdapter logs an error for unhandled S3 errors."""
mock_client = MagicMock(spec=Minio) mock_client = MagicMock(spec=Minio)
mock_client.bucket_exists.return_value = True mock_client.bucket_exists.return_value = True
other_s3error = S3Error( 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 mock_client.get_object.side_effect = other_s3error
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client) adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
with caplog.at_level("ERROR"): with pytest.raises(S3Error) as exc_info:
result = adapter.get("missing-object") adapter.get("missing-object")
assert result is None assert exc_info.value.code == "UnhandledError"
assert repr(other_s3error) in caplog.text
def test_should_log_error_when_getting_with_general_exception( def test_should_reraise_general_exception() -> None:
caplog: pytest.LogCaptureFixture, """Test that the MinioAdapter re-raises general exceptions during get."""
) -> None:
"""Test that the MinioAdapter logs an error on general exceptions during get."""
mock_client = MagicMock(spec=Minio) mock_client = MagicMock(spec=Minio)
mock_client.bucket_exists.return_value = True mock_client.bucket_exists.return_value = True
general_exception = Exception("General failure") general_exception = Exception("General failure")
mock_client.get_object.side_effect = general_exception mock_client.get_object.side_effect = general_exception
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client) adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
with caplog.at_level("ERROR"): with pytest.raises(Exception, match="General failure"):
result = adapter.get("missing-object") adapter.get("missing-object")
assert result is None
assert repr(general_exception) in caplog.text
def test_should_put_data( def test_should_put_data(
+59 -3
View File
@@ -5,7 +5,8 @@ from __future__ import annotations
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest 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.adapters.minio_adapter import MinioAdapter
from python_repositories.interfaces import ObjectRepositoryInterface 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 mock_client.get_object.return_value = mock_response
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client) 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.close.assert_called_once()
mock_response.release_conn.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")