Compare commits

...
3 Commits
Author SHA1 Message Date
Brian Bjarke Jensen 800d702d7a pyupgrade
Test Python Package / test (pull_request) Failing after 11s
Code Quality Pipeline / code-quality (pull_request) Failing after 43s
2025-09-15 00:54:01 +02:00
Brian Bjarke Jensen 5a72507775 ruff format 2025-09-15 00:53:38 +02:00
Brian Bjarke Jensen c45ba3c5d5 mypy fixes 2025-09-15 00:53:26 +02:00
5 changed files with 82 additions and 45 deletions
+4
View File
@@ -60,6 +60,10 @@ namespace_packages = true # enable namespace packages
module = "testcontainers.*" module = "testcontainers.*"
ignore_missing_imports = true ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "minio.*"
ignore_missing_imports = true
[dependency-groups] [dependency-groups]
dev = [ dev = [
"mypy>=1.17.1", "mypy>=1.17.1",
+23 -11
View File
@@ -25,7 +25,7 @@ class MinioAdapter(
access_key_env_var_name: str = "MINIO_ACCESS_KEY" access_key_env_var_name: str = "MINIO_ACCESS_KEY"
secret_key_env_var_name: str = "MINIO_SECRET_KEY" secret_key_env_var_name: str = "MINIO_SECRET_KEY"
bucket_env_var_name: str = "MINIO_BUCKET" bucket_env_var_name: str = "MINIO_BUCKET"
chunk_size: int = 5*2**20 # 5 MiB chunk_size: int = 5 * 2**20 # 5 MiB
def __init__(self) -> None: def __init__(self) -> None:
# Setup logger # Setup logger
@@ -127,13 +127,15 @@ class MinioAdapter(
# Send data to bucket # Send data to bucket
# N.B. bucket name is set when connecting # N.B. bucket name is set when connecting
self._client.put_object( self._client.put_object(
bucket_name=self._bucket_name, # type: ignore bucket_name=self._bucket_name,
object_name=object_name, object_name=object_name,
data=data, data=data,
length=num_bytes, length=num_bytes,
part_size=self.chunk_size, part_size=self.chunk_size,
) )
self.logger.debug(f"Put object '{object_name}' into bucket '{self._bucket_name}'") self.logger.debug(
f"Put object '{object_name}' into bucket '{self._bucket_name}'"
)
def _get(self, object_name: str) -> BytesIO | None: def _get(self, object_name: str) -> BytesIO | None:
"""Get an object from the Minio bucket.""" """Get an object from the Minio bucket."""
@@ -147,7 +149,7 @@ class MinioAdapter(
# N.B. bucket name is set when connecting # N.B. bucket name is set when connecting
try: try:
response = self._client.get_object( response = self._client.get_object(
bucket_name=self._bucket_name, # type: ignore bucket_name=self._bucket_name,
object_name=object_name, object_name=object_name,
) )
# Get buffered data # Get buffered data
@@ -155,11 +157,15 @@ class MinioAdapter(
while chunk := response.read(self.chunk_size): while chunk := response.read(self.chunk_size):
buffer.write(chunk) buffer.write(chunk)
buffer.seek(0) buffer.seek(0)
self.logger.debug(f"Got object '{object_name}' from bucket '{self._bucket_name}'") self.logger.debug(
f"Got object '{object_name}' from bucket '{self._bucket_name}'"
)
return buffer return buffer
except S3Error as exc: except S3Error as exc:
if exc.code == "NoSuchKey": if exc.code == "NoSuchKey":
self.logger.warning(f"Object '{object_name}' not found in bucket '{self._bucket_name}'") self.logger.warning(
f"Object '{object_name}' not found in bucket '{self._bucket_name}'"
)
else: else:
self.logger.error(repr(exc)) self.logger.error(repr(exc))
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
@@ -177,10 +183,12 @@ class MinioAdapter(
# Delete object from bucket # Delete object from bucket
# N.B. bucket name is set when connecting # N.B. bucket name is set when connecting
self._client.remove_object( self._client.remove_object(
bucket_name=self._bucket_name, # type: ignore bucket_name=self._bucket_name,
object_name=object_name, object_name=object_name,
) )
self.logger.debug(f"Deleted object '{object_name}' from bucket '{self._bucket_name}'") self.logger.debug(
f"Deleted object '{object_name}' from bucket '{self._bucket_name}'"
)
def _list_objects(self, prefix: str = "") -> list[str]: def _list_objects(self, prefix: str = "") -> list[str]:
"""List objects in the Minio bucket with an optional prefix.""" """List objects in the Minio bucket with an optional prefix."""
@@ -193,10 +201,14 @@ class MinioAdapter(
raise ConnectionError("Not connected to Minio") raise ConnectionError("Not connected to Minio")
# List objects in bucket # List objects in bucket
objects = self._client.list_objects( objects = self._client.list_objects(
bucket_name=self._bucket_name, # type: ignore bucket_name=self._bucket_name,
prefix=prefix, prefix=prefix,
recursive=True, recursive=True,
) )
object_names = [obj.object_name for obj in objects if obj.object_name is not None] object_names = [
self.logger.debug(f"Listed {len(object_names)} object(s) in bucket '{self._bucket_name}' with prefix '{prefix}'") obj.object_name for obj in objects if obj.object_name is not None
]
self.logger.debug(
f"Listed {len(object_names)} object(s) in bucket '{self._bucket_name}' with prefix '{prefix}'"
)
return object_names return object_names
+3 -3
View File
@@ -37,7 +37,7 @@ def configure_logging() -> None:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def redis_container() -> Generator[str, None, None]: def redis_container() -> Generator[str]:
"""Set up a Redis container for testing and yield the Redis URI.""" """Set up a Redis container for testing and yield the Redis URI."""
# Start container # Start container
container = RedisContainer( container = RedisContainer(
@@ -56,7 +56,7 @@ def redis_container() -> Generator[str, None, None]:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def minio_container() -> Generator[dict[str, str], None, None]: def minio_container() -> Generator[dict[str, str]]:
"""Set up a Minio container for testing and yield the Minio URI.""" """Set up a Minio container for testing and yield the Minio URI."""
# Start container # Start container
container = MinioContainer( container = MinioContainer(
@@ -138,4 +138,4 @@ def raw_minio_client(minio_container: dict[str, str]) -> Generator[Minio]:
# Cleanup # Cleanup
objects = client.list_objects(bucket_name, recursive=True) objects = client.list_objects(bucket_name, recursive=True)
for obj in objects: for obj in objects:
client.remove_object(bucket_name, obj.object_name) client.remove_object(bucket_name, obj.object_name)
+46 -25
View File
@@ -31,14 +31,14 @@ def same_data(
# compare size # compare size
if len(data_a_bytes) != len(data_b_bytes): if len(data_a_bytes) != len(data_b_bytes):
logging.error( logging.error(
'data has different length: %s and %s', "data has different length: %s and %s",
len(data_a_bytes), len(data_a_bytes),
len(data_b_bytes), len(data_b_bytes),
) )
return False return False
# compare content # compare content
if data_a_bytes != data_b_bytes: if data_a_bytes != data_b_bytes:
logging.error('data has different bytes') logging.error("data has different bytes")
return False return False
return True return True
@@ -187,7 +187,7 @@ def test_should_get_data(
# Arrange # Arrange
object_name, expected_data = data_in_minio object_name, expected_data = data_in_minio
# Act # Act
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
# Assert # Assert
assert received_data is not None assert received_data is not None
assert same_data(expected_data, received_data) assert same_data(expected_data, received_data)
@@ -200,7 +200,7 @@ def test_should_get_none_for_nonexistent_object(
# Arrange # Arrange
object_name = "nonexistent_object" object_name = "nonexistent_object"
# Act # Act
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
# Assert # Assert
assert received_data is None assert received_data is None
@@ -225,7 +225,7 @@ def test_should_raise_connection_error_on_get_when_not_connected(
adapter = MinioAdapter() # not connected adapter = MinioAdapter() # not connected
# Act & Assert # Act & Assert
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
adapter._get("some_object") # type: ignore adapter._get("some_object")
def test_should_log_warning_when_getting_nonexistent_object( def test_should_log_warning_when_getting_nonexistent_object(
@@ -235,7 +235,16 @@ def test_should_log_warning_when_getting_nonexistent_object(
# Arrange # Arrange
adapter = MinioAdapter() adapter = MinioAdapter()
adapter._client = MagicMock(spec=Minio) adapter._client = MagicMock(spec=Minio)
adapter._client.get_object.side_effect = S3Error(code="NoSuchKey", message="", resource="", request_id="", host_id="", response="", bucket_name="test-bucket", object_name="missing-object") adapter._client.get_object.side_effect = S3Error(
code="NoSuchKey",
message="",
resource="",
request_id="",
host_id="",
response="",
bucket_name="test-bucket",
object_name="missing-object",
)
adapter._bucket_name = "test-bucket" adapter._bucket_name = "test-bucket"
object_name = "missing-object" object_name = "missing-object"
# Act # Act
@@ -243,7 +252,10 @@ def test_should_log_warning_when_getting_nonexistent_object(
result = adapter._get(object_name) result = adapter._get(object_name)
# Assert # Assert
assert result is None assert result is None
assert f"Object '{object_name}' not found in bucket '{adapter._bucket_name}'" in caplog.text assert (
f"Object '{object_name}' not found in bucket '{adapter._bucket_name}'"
in caplog.text
)
def test_should_log_error_when_getting_with_s3error_other_than_no_such_key( def test_should_log_error_when_getting_with_s3error_other_than_no_such_key(
@@ -253,7 +265,16 @@ def test_should_log_error_when_getting_with_s3error_other_than_no_such_key(
# Arrange # Arrange
adapter = MinioAdapter() adapter = MinioAdapter()
adapter._client = MagicMock(spec=Minio) adapter._client = MagicMock(spec=Minio)
other_s3error = S3Error(code="UnhandledError", message="", resource="", request_id="", host_id="", response="", bucket_name="test-bucket", object_name="missing-object") other_s3error = S3Error(
code="UnhandledError",
message="",
resource="",
request_id="",
host_id="",
response="",
bucket_name="test-bucket",
object_name="missing-object",
)
adapter._client.get_object.side_effect = other_s3error adapter._client.get_object.side_effect = other_s3error
adapter._bucket_name = "test-bucket" adapter._bucket_name = "test-bucket"
object_name = "missing-object" object_name = "missing-object"
@@ -291,17 +312,17 @@ def test_should_put_data(
"""Test that the MinioAdapter can put data into a bucket.""" """Test that the MinioAdapter can put data into a bucket."""
# Arrange # Arrange
object_name = "new_test_object" object_name = "new_test_object"
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
assert received_data is None # ensure object does not exist yet assert received_data is None # ensure object does not exist yet
# Act # Act
minio_adapter._put(object_name, data) # type: ignore minio_adapter._put(object_name, data)
# Assert # Assert
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
assert received_data is not None assert received_data is not None
assert same_data(data, received_data) assert same_data(data, received_data)
# Cleanup # Cleanup
bucket_name = str(os.getenv("MINIO_BUCKET")) bucket_name = str(os.getenv("MINIO_BUCKET"))
minio_adapter._client.remove_object(bucket_name, object_name) minio_adapter._client.remove_object(bucket_name, object_name) # type: ignore
def test_should_update_data( def test_should_update_data(
@@ -312,13 +333,13 @@ def test_should_update_data(
# Arrange # Arrange
object_name, _ = data_in_minio object_name, _ = data_in_minio
new_data = BytesIO(random.randbytes(2**21)) # 2 MiB new_data = BytesIO(random.randbytes(2**21)) # 2 MiB
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
assert received_data is not None assert received_data is not None
assert not same_data(received_data, new_data) assert not same_data(received_data, new_data)
# Act # Act
minio_adapter._put(object_name, new_data) # type: ignore minio_adapter._put(object_name, new_data)
# Assert # Assert
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
assert received_data is not None assert received_data is not None
assert same_data(new_data, received_data) assert same_data(new_data, received_data)
@@ -358,7 +379,7 @@ def test_should_raise_connection_error_on_put_when_not_connected(
adapter = MinioAdapter() # not connected adapter = MinioAdapter() # not connected
# Act & Assert # Act & Assert
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
adapter._put("some_object", data) # type: ignore adapter._put("some_object", data)
def test_should_delete_object( def test_should_delete_object(
@@ -368,12 +389,12 @@ def test_should_delete_object(
"""Test that the MinioAdapter can delete an object from a bucket.""" """Test that the MinioAdapter can delete an object from a bucket."""
# Arrange # Arrange
object_name, _ = data_in_minio object_name, _ = data_in_minio
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
assert received_data is not None # ensure object exists assert received_data is not None # ensure object exists
# Act # Act
minio_adapter._delete(object_name) # type: ignore minio_adapter._delete(object_name)
# Assert # Assert
received_data = minio_adapter._get(object_name) # type: ignore received_data = minio_adapter._get(object_name)
assert received_data is None assert received_data is None
@@ -397,7 +418,7 @@ def test_should_raise_connection_error_on_delete_when_not_connected(
adapter = MinioAdapter() # not connected adapter = MinioAdapter() # not connected
# Act & Assert # Act & Assert
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
adapter._delete("some_object") # type: ignore adapter._delete("some_object")
def test_should_list_objects( def test_should_list_objects(
@@ -409,9 +430,9 @@ def test_should_list_objects(
object_name, _ = data_in_minio object_name, _ = data_in_minio
new_data = BytesIO(random.randbytes(2**21)) # 2 MiB new_data = BytesIO(random.randbytes(2**21)) # 2 MiB
new_data_name = "another_test_object" new_data_name = "another_test_object"
minio_adapter._put(new_data_name, new_data) # type: ignore minio_adapter._put(new_data_name, new_data)
# Act # Act
objects = minio_adapter._list_objects() # type: ignore objects = minio_adapter._list_objects()
# Assert # Assert
assert isinstance(objects, list) assert isinstance(objects, list)
assert len(objects) == 2 assert len(objects) == 2
@@ -428,10 +449,10 @@ def test_should_list_objects_with_prefix(
object_name, _ = data_in_minio object_name, _ = data_in_minio
new_data = BytesIO(random.randbytes(2**21)) # 2 MiB new_data = BytesIO(random.randbytes(2**21)) # 2 MiB
new_data_name = "prefix_test_object" new_data_name = "prefix_test_object"
minio_adapter._put(new_data_name, new_data) # type: ignore minio_adapter._put(new_data_name, new_data)
prefix = "prefix_" prefix = "prefix_"
# Act # Act
objects = minio_adapter._list_objects(prefix) # type: ignore objects = minio_adapter._list_objects(prefix)
# Assert # Assert
assert isinstance(objects, list) assert isinstance(objects, list)
assert len(objects) == 1 assert len(objects) == 1
@@ -459,7 +480,7 @@ def test_should_raise_connection_error_on_list_objects_when_not_connected(
adapter = MinioAdapter() # not connected adapter = MinioAdapter() # not connected
# Act & Assert # Act & Assert
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
adapter._list_objects() # type: ignore adapter._list_objects()
# allows local debugging by running file as script # allows local debugging by running file as script
+6 -6
View File
@@ -134,7 +134,7 @@ def test_should_get_value(
# Arrange # Arrange
key, data = data_in_redis key, data = data_in_redis
# Act # Act
value = redis_adapter._get(key) # type: ignore value = redis_adapter._get(key)
# Assert # Assert
assert value is not None assert value is not None
assert value == data assert value == data
@@ -145,7 +145,7 @@ def test_should_get_none_for_missing_key(
) -> None: ) -> None:
"""Test that getting a non-existent key returns None.""" """Test that getting a non-existent key returns None."""
# Act # Act
value = redis_adapter._get("nonexistent_key") # type: ignore value = redis_adapter._get("nonexistent_key")
# Assert # Assert
assert value is None assert value is None
@@ -180,12 +180,12 @@ def test_should_set_value(
"""Test that the RedisAdapter can set a value.""" """Test that the RedisAdapter can set a value."""
# Arrange # Arrange
key = "test_key" key = "test_key"
received_data = redis_adapter._get(key) # type: ignore received_data = redis_adapter._get(key)
assert received_data is None # Ensure key does not exist assert received_data is None # Ensure key does not exist
# Act # Act
redis_adapter._set(key, data) redis_adapter._set(key, data)
# Assert # Assert
received_data = redis_adapter._get(key) # type: ignore received_data = redis_adapter._get(key)
assert received_data is not None assert received_data is not None
assert received_data == data assert received_data == data
@@ -198,7 +198,7 @@ def test_should_update_value(
# Arrange # Arrange
key, _ = data_in_redis key, _ = data_in_redis
new_data = {"new_key": "new_value"} new_data = {"new_key": "new_value"}
received_data = redis_adapter._get(key) # type: ignore received_data = redis_adapter._get(key)
assert received_data is not None assert received_data is not None
assert received_data != new_data assert received_data != new_data
# Act # Act
@@ -253,7 +253,7 @@ def test_should_delete_key(
"""Test that deleting a key removes it from Redis.""" """Test that deleting a key removes it from Redis."""
# Arrange # Arrange
key, _ = data_in_redis key, _ = data_in_redis
received_data = redis_adapter._get(key) # type: ignore received_data = redis_adapter._get(key)
assert received_data is not None # Ensure key exists assert received_data is not None # Ensure key exists
# Act # Act
redis_adapter._delete(key) redis_adapter._delete(key)