Compare commits

..
4 Commits
Author SHA1 Message Date
CI Bot fe0c477dff chore: release v0.4.0 [skip ci] 2026-07-05 13:41:59 +00:00
brian e7e0fc8c1d Merge pull request '[minor] Strengthen is_connected with cached health probes' (#22) from cursor/strengthen-is-connected-health-checks into main
Code Quality Pipeline / code-quality (push) Successful in 50s
Release on merge to main / release (push) Successful in 8s
Test Python Package / test (push) Successful in 1m12s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/22
2026-07-05 15:41:00 +02:00
Brian Bjarke JensenandCursor 34fc046868 Sync uv.lock with pyproject.toml version 0.3.2.
Code Quality Pipeline / code-quality (pull_request) Successful in 31s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Successful in 58s
Co-authored-by: Cursor <[email protected]>
2026-07-05 15:38:49 +02:00
Brian Bjarke JensenandCursor 5149299c1b Strengthen is_connected with cached health probes.
Code Quality Pipeline / code-quality (pull_request) Failing after 35s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Successful in 1m9s
Convert is_connected to a method that verifies backend liveness via TTL-cached ping (Redis) or bucket_exists (MinIO), with cache invalidation on connect/disconnect.

Co-authored-by: Cursor <[email protected]>
2026-07-05 15:28:25 +02:00
10 changed files with 291 additions and 36 deletions
+2
View File
@@ -12,6 +12,8 @@ Subclass an adapter in your own repository to add domain-specific methods while
| **Adapters** | Technology-specific base classes (`RedisAdapter`, `MinioAdapter`) | | **Adapters** | Technology-specific base classes (`RedisAdapter`, `MinioAdapter`) |
| **Your project** | Subclass an adapter and add domain methods | | **Your project** | Subclass an adapter and add domain methods |
Connection adapters expose `connect()`, `disconnect()`, and `is_connected()`. The latter verifies backend reachability with a cached health probe (default TTL: 1 second). Subclasses may override `health_check_ttl_seconds`.
## Optional dependencies ## Optional dependencies
Repository **interfaces** import with the base package. **Adapters** require the matching extra; importing an adapter without its extra raises `ImportError` with install instructions. Repository **interfaces** import with the base package. **Adapters** require the matching extra; importing an adapter without its extra raises `ImportError` with install instructions.
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "python-repositories" name = "python-repositories"
version = "0.3.2" version = "0.4.0"
description = "Various python repository interfaces exposed as a python package." description = "Various python repository interfaces exposed as a python package."
authors = [ authors = [
{ name = "Brian Bjarke Jensen", email = "[email protected]" } { name = "Brian Bjarke Jensen", email = "[email protected]" }
+45 -12
View File
@@ -2,9 +2,10 @@
from __future__ import annotations from __future__ import annotations
import os import os
import structlog
import time
from io import BytesIO from io import BytesIO
from typing import Self from typing import Self
import structlog
from python_utils import check_env from python_utils import check_env
from python_repositories.interfaces import ( from python_repositories.interfaces import (
@@ -34,6 +35,7 @@ class MinioAdapter(
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
health_check_ttl_seconds: float = 1.0
def __init__(self) -> None: def __init__(self) -> None:
# Setup logger # Setup logger
@@ -52,6 +54,8 @@ class MinioAdapter(
# Prepare internal variables # Prepare internal variables
self._client: minio.Minio | None = None self._client: minio.Minio | None = None
self._bucket_name: str | None = None self._bucket_name: str | None = None
self._health_check_at: float | None = None
self._health_check_ok: bool = False
def __enter__(self) -> Self: def __enter__(self) -> Self:
"""Enter the context.""" """Enter the context."""
@@ -75,10 +79,11 @@ class MinioAdapter(
def connect(self) -> None: def connect(self) -> None:
"""Connect to the Minio server.""" """Connect to the Minio server."""
# Stop if already connected if self._client is not None and self.is_connected():
if self.is_connected:
self.logger.info("Already connected to Minio") self.logger.info("Already connected to Minio")
return return
if self._client is not None:
self.disconnect()
# Prepare arguments # Prepare arguments
endpoint = str(os.getenv(self.endpoint_env_var_name)) endpoint = str(os.getenv(self.endpoint_env_var_name))
access_key = str(os.getenv(self.access_key_env_var_name)) access_key = str(os.getenv(self.access_key_env_var_name))
@@ -103,6 +108,7 @@ class MinioAdapter(
# Persist information # Persist information
self._client = client self._client = client
self._bucket_name = bucket self._bucket_name = bucket
self._invalidate_health_cache()
def disconnect(self) -> None: def disconnect(self) -> None:
"""Disconnect from the Minio server.""" """Disconnect from the Minio server."""
@@ -111,13 +117,36 @@ class MinioAdapter(
# Reset client # Reset client
self._client = None self._client = None
self._bucket_name = None self._bucket_name = None
self._invalidate_health_cache()
def _invalidate_health_cache(self) -> None:
self._health_check_at = None
self._health_check_ok = False
def _probe_connection(self) -> bool:
assert self._client is not None and self._bucket_name is not None
try:
return bool(self._client.bucket_exists(self._bucket_name))
except Exception: # pylint: disable=broad-except
return False
@property
def is_connected(self) -> bool: def is_connected(self) -> bool:
"""Check if connected to Minio server.""" """Check if connected to the Minio server."""
res = self._client is not None if self._client is None or self._bucket_name is None:
self.logger.debug(res) return False
return res now = time.monotonic()
if self._health_check_at is not None:
seconds_since_last_health_check = now - self._health_check_at
cache_is_fresh = (
seconds_since_last_health_check < self.health_check_ttl_seconds
)
if cache_is_fresh:
return self._health_check_ok
result = self._probe_connection()
self._health_check_at = now
self._health_check_ok = result
self.logger.debug("Connection status", connected=result)
return result
def put( def put(
self, self,
@@ -134,8 +163,9 @@ class MinioAdapter(
if not isinstance(content_type, str) or len(content_type) == 0: if not isinstance(content_type, str) or len(content_type) == 0:
raise ValueError("content_type must be a non-empty string") raise ValueError("content_type must be a non-empty string")
# Check connection # Check connection
if self._client is None or self._bucket_name is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Minio") raise ConnectionError("Not connected to Minio")
assert self._client is not None and self._bucket_name is not None
# Prepare buffer for reading # Prepare buffer for reading
num_bytes = data.getbuffer().nbytes num_bytes = data.getbuffer().nbytes
data.seek(0) data.seek(0)
@@ -159,8 +189,9 @@ class MinioAdapter(
if not isinstance(object_name, str) or len(object_name) == 0: if not isinstance(object_name, str) or len(object_name) == 0:
raise ValueError("object_name must be a non-empty string") raise ValueError("object_name must be a non-empty string")
# Check connection # Check connection
if self._client is None or self._bucket_name is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Minio") raise ConnectionError("Not connected to Minio")
assert self._client is not None and self._bucket_name is not None
# Get data from bucket # Get data from bucket
# N.B. bucket name is set when connecting # N.B. bucket name is set when connecting
try: try:
@@ -194,8 +225,9 @@ class MinioAdapter(
if not isinstance(object_name, str) or len(object_name) == 0: if not isinstance(object_name, str) or len(object_name) == 0:
raise ValueError("object_name must be a non-empty string") raise ValueError("object_name must be a non-empty string")
# Check connection # Check connection
if self._client is None or self._bucket_name is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Minio") raise ConnectionError("Not connected to Minio")
assert self._client is not None and self._bucket_name is not None
# 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(
@@ -213,8 +245,9 @@ class MinioAdapter(
raise ValueError("prefix must be a string") raise ValueError("prefix must be a string")
# Check connection # Check connection
# N.B. bucket name is set when connecting # N.B. bucket name is set when connecting
if self._client is None or self._bucket_name is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Minio") raise ConnectionError("Not connected to Minio")
assert self._client is not None and self._bucket_name is not None
# List objects in bucket # List objects in bucket
objects = self._client.list_objects( objects = self._client.list_objects(
bucket_name=self._bucket_name, bucket_name=self._bucket_name,
+45 -9
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Self, cast from typing import Self, cast
import os import os
import structlog import structlog
import time
from python_utils import check_env from python_utils import check_env
@@ -33,6 +34,7 @@ class RedisAdapter(
uri_env_var_name: str = "REDIS_URI" uri_env_var_name: str = "REDIS_URI"
path: str = "." # JSON root path, updated in __init__ path: str = "." # JSON root path, updated in __init__
encoding: str = "UTF-8" encoding: str = "UTF-8"
health_check_ttl_seconds: float = 1.0
def __init__(self) -> None: def __init__(self) -> None:
# Setup logger # Setup logger
@@ -44,6 +46,8 @@ class RedisAdapter(
# Prepare internal variables # Prepare internal variables
self._client: redis.Redis | None = None self._client: redis.Redis | None = None
self.path: str = RedisPath.root_path() self.path: str = RedisPath.root_path()
self._health_check_at: float | None = None
self._health_check_ok: bool = False
def __enter__(self) -> Self: def __enter__(self) -> Self:
"""Enter the context.""" """Enter the context."""
@@ -67,6 +71,10 @@ class RedisAdapter(
def connect(self) -> None: def connect(self) -> None:
"""Connect to the Redis server.""" """Connect to the Redis server."""
if self._client is not None:
self._client.close()
self._client = None
self._invalidate_health_cache()
# Prepare arguments # Prepare arguments
uri = str(os.getenv(self.uri_env_var_name)) uri = str(os.getenv(self.uri_env_var_name))
# Connect client # Connect client
@@ -81,6 +89,7 @@ class RedisAdapter(
raise ConnectionError(f"Could not connect to Redis at {uri}") from exc raise ConnectionError(f"Could not connect to Redis at {uri}") from exc
# Persist client # Persist client
self._client = client self._client = client
self._invalidate_health_cache()
def disconnect(self) -> None: def disconnect(self) -> None:
"""Disconnect from the Redis server.""" """Disconnect from the Redis server."""
@@ -89,13 +98,36 @@ class RedisAdapter(
self._client.close() self._client.close()
# Reset client # Reset client
self._client = None self._client = None
self._invalidate_health_cache()
def _invalidate_health_cache(self) -> None:
self._health_check_at = None
self._health_check_ok = False
def _probe_connection(self) -> bool:
assert self._client is not None
try:
return bool(self._client.ping())
except (redis.ConnectionError, redis.TimeoutError):
return False
@property
def is_connected(self) -> bool: def is_connected(self) -> bool:
"""Check if connected to Redis server.""" """Check if connected to the Redis server."""
res = self._client is not None if self._client is None:
self.logger.debug(res) return False
return res now = time.monotonic()
if self._health_check_at is not None:
seconds_since_last_health_check = now - self._health_check_at
cache_is_fresh = (
seconds_since_last_health_check < self.health_check_ttl_seconds
)
if cache_is_fresh:
return self._health_check_ok
result = self._probe_connection()
self._health_check_at = now
self._health_check_ok = result
self.logger.debug("Connection status", connected=result)
return result
def set(self, key: str, data: dict) -> None: def set(self, key: str, data: dict) -> None:
"""Set a JSON object in Redis.""" """Set a JSON object in Redis."""
@@ -105,8 +137,9 @@ class RedisAdapter(
if not isinstance(data, dict) or len(data) == 0: if not isinstance(data, dict) or len(data) == 0:
raise ValueError("Data must be a non-empty dictionary") raise ValueError("Data must be a non-empty dictionary")
# Check connection # Check connection
if self._client is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Redis") raise ConnectionError("Not connected to Redis")
assert self._client is not None
# Set data # Set data
self._client.json().set(key, self.path, data) self._client.json().set(key, self.path, data)
self.logger.debug(f"Set {key} to {data}") self.logger.debug(f"Set {key} to {data}")
@@ -117,8 +150,9 @@ class RedisAdapter(
if not isinstance(key, str) or len(key) == 0: if not isinstance(key, str) or len(key) == 0:
raise ValueError("Key must be a non-empty string") raise ValueError("Key must be a non-empty string")
# Check connection # Check connection
if self._client is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Redis") raise ConnectionError("Not connected to Redis")
assert self._client is not None
# Get data # Get data
data = cast( data = cast(
dict | None, dict | None,
@@ -133,8 +167,9 @@ class RedisAdapter(
if not isinstance(key, str) or len(key) == 0: if not isinstance(key, str) or len(key) == 0:
raise ValueError("Key must be a non-empty string") raise ValueError("Key must be a non-empty string")
# Check connection # Check connection
if self._client is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Redis") raise ConnectionError("Not connected to Redis")
assert self._client is not None
# Delete data # Delete data
self._client.json().delete(key) self._client.json().delete(key)
self.logger.debug(f"Deleted {key}") self.logger.debug(f"Deleted {key}")
@@ -145,8 +180,9 @@ class RedisAdapter(
if not isinstance(pattern, str) or len(pattern) == 0: if not isinstance(pattern, str) or len(pattern) == 0:
raise ValueError("Pattern must be a non-empty string") raise ValueError("Pattern must be a non-empty string")
# Check connection # Check connection
if self._client is None or not self.is_connected: if not self.is_connected():
raise ConnectionError("Not connected to Redis") raise ConnectionError("Not connected to Redis")
assert self._client is not None
# List keys # List keys
keys_raw = cast( keys_raw = cast(
list[bytes], list[bytes],
@@ -16,8 +16,10 @@ class ConnectionAwareInterface(ABC):
"""Disconnect from resource.""" """Disconnect from resource."""
... ...
@property
@abstractmethod @abstractmethod
def is_connected(self) -> bool: def is_connected(self) -> bool:
"""Check if connected to resource.""" """Return whether the adapter has an active, reachable connection.
Implementations may perform a cached network probe to verify liveness.
"""
... ...
@@ -15,7 +15,6 @@ def test_instantiation_fails_when_connect_not_implemented() -> None:
def disconnect(self) -> None: def disconnect(self) -> None:
pass pass
@property
def is_connected(self) -> bool: def is_connected(self) -> bool:
return False return False
@@ -32,7 +31,6 @@ def test_instantiation_fails_when_disconnect_not_implemented() -> None:
def connect(self) -> None: def connect(self) -> None:
pass pass
@property
def is_connected(self) -> bool: def is_connected(self) -> bool:
return False return False
+3 -3
View File
@@ -115,7 +115,7 @@ def test_should_have_logger_when_instantiated() -> None:
def test_should_not_be_connected_when_instantiated() -> None: def test_should_not_be_connected_when_instantiated() -> None:
"""Test that the MinioAdapter is not connected when instantiated.""" """Test that the MinioAdapter is not connected when instantiated."""
adapter = MinioAdapter() adapter = MinioAdapter()
assert not adapter.is_connected assert not adapter.is_connected()
def test_should_log_info_when_already_connected( def test_should_log_info_when_already_connected(
@@ -137,7 +137,7 @@ def test_should_raise_connection_error_when_unable_to_connect(
adapter = MinioAdapter() adapter = MinioAdapter()
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
adapter.connect() adapter.connect()
assert not adapter.is_connected assert not adapter.is_connected()
def test_should_log_info_when_creating_expected_bucket( def test_should_log_info_when_creating_expected_bucket(
@@ -163,7 +163,7 @@ def test_should_log_error_on_exception_during_exit(
"""Test that the MinioAdapter logs an error if an exception occurs during __exit__.""" """Test that the MinioAdapter logs an error if an exception occurs during __exit__."""
try: try:
with MinioAdapter() as adapter: with MinioAdapter() as adapter:
assert adapter.is_connected assert adapter.is_connected()
raise ValueError("Simulated error") raise ValueError("Simulated error")
except ValueError: except ValueError:
pass # Expected pass # Expected
+6 -6
View File
@@ -9,7 +9,7 @@ from python_repositories.interfaces import JsonRepositoryInterface
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def data() -> Generator[dict[str, str]]: def data() -> Generator[dict[str, str], None, None]:
"""Provide a sample data dictionary for tests.""" """Provide a sample data dictionary for tests."""
yield {"foo": "bar"} yield {"foo": "bar"}
@@ -18,7 +18,7 @@ def data() -> Generator[dict[str, str]]:
def data_in_redis( def data_in_redis(
raw_redis_client: redis.Redis, raw_redis_client: redis.Redis,
data: dict[str, str], data: dict[str, str],
) -> Generator[tuple[str, dict[str, str]]]: ) -> Generator[tuple[str, dict[str, str]], None, None]:
"""Fixture to set up a known value in Redis before each test.""" """Fixture to set up a known value in Redis before each test."""
key = "test_key" key = "test_key"
path = RedisPath.root_path() path = RedisPath.root_path()
@@ -31,7 +31,7 @@ def data_in_redis(
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def redis_adapter(redis_container: str) -> Generator[RedisAdapter]: def redis_adapter(redis_container: str) -> Generator[RedisAdapter, None, None]:
"""Fixture to provide a connected RedisAdapter instance.""" """Fixture to provide a connected RedisAdapter instance."""
adapter = RedisAdapter() adapter = RedisAdapter()
adapter.connect() adapter.connect()
@@ -63,7 +63,7 @@ def test_should_not_be_connected_when_instantiated(redis_container: str) -> None
"""Test that the RedisAdapter is not connected when instantiated.""" """Test that the RedisAdapter is not connected when instantiated."""
adapter = RedisAdapter() adapter = RedisAdapter()
assert adapter._client is None assert adapter._client is None
assert not adapter.is_connected assert not adapter.is_connected()
def test_should_raise_connection_error_when_unable_to_connect( def test_should_raise_connection_error_when_unable_to_connect(
@@ -77,7 +77,7 @@ def test_should_raise_connection_error_when_unable_to_connect(
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
adapter.connect() adapter.connect()
assert adapter._client is None assert adapter._client is None
assert not adapter.is_connected assert not adapter.is_connected()
def test_connect_raises_connection_error_when_unable_to_ping( def test_connect_raises_connection_error_when_unable_to_ping(
@@ -109,7 +109,7 @@ def test_should_log_error_on_exception_during_exit(
"""Test that the RedisAdapter logs an error when an exception occurs during context exit.""" """Test that the RedisAdapter logs an error when an exception occurs during context exit."""
try: try:
with RedisAdapter() as adapter: with RedisAdapter() as adapter:
assert adapter.is_connected assert adapter.is_connected()
raise ValueError("Simulated error") raise ValueError("Simulated error")
except ValueError: except ValueError:
pass # Expected pass # Expected
+184
View File
@@ -0,0 +1,184 @@
"""Tests for TTL-cached connection health checks on adapters."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
import redis
from python_repositories.adapters.minio_adapter import MinioAdapter
from python_repositories.adapters.redis_adapter import RedisAdapter
@pytest.fixture
def redis_adapter(monkeypatch: pytest.MonkeyPatch) -> RedisAdapter:
monkeypatch.setenv("REDIS_URI", "redis://localhost:6379")
return RedisAdapter()
@pytest.fixture
def minio_adapter(monkeypatch: pytest.MonkeyPatch) -> MinioAdapter:
monkeypatch.setenv("MINIO_ENDPOINT", "localhost:9000")
monkeypatch.setenv("MINIO_ACCESS_KEY", "minioadmin")
monkeypatch.setenv("MINIO_SECRET_KEY", "minioadmin")
monkeypatch.setenv("MINIO_BUCKET", "test-bucket")
return MinioAdapter()
class TestRedisConnectionHealth:
def test_not_connected_when_no_client(self, redis_adapter: RedisAdapter) -> None:
assert not redis_adapter.is_connected()
def test_connected_when_probe_succeeds(self, redis_adapter: RedisAdapter) -> None:
mock_client = MagicMock(spec=redis.Redis)
mock_client.ping.return_value = True
redis_adapter._client = mock_client
assert redis_adapter.is_connected()
mock_client.ping.assert_called_once()
def test_stale_connection_when_probe_fails(
self, redis_adapter: RedisAdapter
) -> None:
mock_client = MagicMock(spec=redis.Redis)
mock_client.ping.side_effect = redis.ConnectionError("connection lost")
redis_adapter._client = mock_client
assert not redis_adapter.is_connected()
def test_cache_hit_avoids_second_probe(self, redis_adapter: RedisAdapter) -> None:
mock_client = MagicMock(spec=redis.Redis)
mock_client.ping.return_value = True
redis_adapter._client = mock_client
with patch(
"python_repositories.adapters.redis_adapter.time.monotonic",
return_value=100.0,
):
assert redis_adapter.is_connected()
assert redis_adapter.is_connected()
mock_client.ping.assert_called_once()
def test_cache_miss_runs_probe_again(self, redis_adapter: RedisAdapter) -> None:
mock_client = MagicMock(spec=redis.Redis)
mock_client.ping.return_value = True
redis_adapter._client = mock_client
with patch(
"python_repositories.adapters.redis_adapter.time.monotonic",
side_effect=[100.0, 102.0],
):
assert redis_adapter.is_connected()
assert redis_adapter.is_connected()
assert mock_client.ping.call_count == 2
def test_disconnect_clears_cache(self, redis_adapter: RedisAdapter) -> None:
mock_client = MagicMock(spec=redis.Redis)
mock_client.ping.return_value = True
redis_adapter._client = mock_client
with patch(
"python_repositories.adapters.redis_adapter.time.monotonic",
return_value=100.0,
):
assert redis_adapter.is_connected()
redis_adapter.disconnect()
redis_adapter._client = mock_client
with patch(
"python_repositories.adapters.redis_adapter.time.monotonic",
return_value=100.0,
):
assert redis_adapter.is_connected()
assert mock_client.ping.call_count == 2
class TestMinioConnectionHealth:
def test_not_connected_when_no_client(self, minio_adapter: MinioAdapter) -> None:
assert not minio_adapter.is_connected()
def test_not_connected_when_bucket_name_missing(
self, minio_adapter: MinioAdapter
) -> None:
minio_adapter._client = MagicMock()
minio_adapter._bucket_name = None
assert not minio_adapter.is_connected()
def test_connected_when_probe_succeeds(self, minio_adapter: MinioAdapter) -> None:
mock_client = MagicMock()
mock_client.bucket_exists.return_value = True
minio_adapter._client = mock_client
minio_adapter._bucket_name = "test-bucket"
assert minio_adapter.is_connected()
mock_client.bucket_exists.assert_called_once_with("test-bucket")
def test_stale_connection_when_probe_fails(
self, minio_adapter: MinioAdapter
) -> None:
mock_client = MagicMock()
mock_client.bucket_exists.side_effect = Exception("connection lost")
minio_adapter._client = mock_client
minio_adapter._bucket_name = "test-bucket"
assert not minio_adapter.is_connected()
def test_cache_hit_avoids_second_probe(self, minio_adapter: MinioAdapter) -> None:
mock_client = MagicMock()
mock_client.bucket_exists.return_value = True
minio_adapter._client = mock_client
minio_adapter._bucket_name = "test-bucket"
with patch(
"python_repositories.adapters.minio_adapter.time.monotonic",
return_value=100.0,
):
assert minio_adapter.is_connected()
assert minio_adapter.is_connected()
mock_client.bucket_exists.assert_called_once()
def test_cache_miss_runs_probe_again(self, minio_adapter: MinioAdapter) -> None:
mock_client = MagicMock()
mock_client.bucket_exists.return_value = True
minio_adapter._client = mock_client
minio_adapter._bucket_name = "test-bucket"
with patch(
"python_repositories.adapters.minio_adapter.time.monotonic",
side_effect=[100.0, 102.0],
):
assert minio_adapter.is_connected()
assert minio_adapter.is_connected()
assert mock_client.bucket_exists.call_count == 2
def test_disconnect_clears_cache(self, minio_adapter: MinioAdapter) -> None:
mock_client = MagicMock()
mock_client.bucket_exists.return_value = True
minio_adapter._client = mock_client
minio_adapter._bucket_name = "test-bucket"
with patch(
"python_repositories.adapters.minio_adapter.time.monotonic",
return_value=100.0,
):
assert minio_adapter.is_connected()
minio_adapter.disconnect()
minio_adapter._client = mock_client
minio_adapter._bucket_name = "test-bucket"
with patch(
"python_repositories.adapters.minio_adapter.time.monotonic",
return_value=100.0,
):
assert minio_adapter.is_connected()
assert mock_client.bucket_exists.call_count == 2
Generated
+1 -1
View File
@@ -1053,7 +1053,7 @@ wheels = [
[[package]] [[package]]
name = "python-repositories" name = "python-repositories"
version = "0.3.1" version = "0.3.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "python-utils" }, { name = "python-utils" },