Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369295b8ae | ||
|
|
95c78596e7 | ||
|
|
42e885edd5 | ||
|
|
bd4794d1c0 | ||
|
|
3dc412cd15 | ||
|
|
b8d88e6703 | ||
|
|
67bb88fbb4 |
@@ -12,7 +12,7 @@ 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`.
|
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`. `connect()` is idempotent: calling it while already connected and healthy is a no-op.
|
||||||
|
|
||||||
## Optional dependencies
|
## Optional dependencies
|
||||||
|
|
||||||
@@ -123,16 +123,18 @@ with ArtifactObjectRepository() as repo:
|
|||||||
### Subclassing in your own project
|
### Subclassing in your own project
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from python_repositories import RedisAdapter
|
from python_repositories import RedisAdapter
|
||||||
|
|
||||||
class UserRepository(RedisAdapter):
|
class UserRepository(RedisAdapter):
|
||||||
def _key(self, user_id: str) -> str:
|
def _key(self, user_id: str) -> str:
|
||||||
return f"user:{user_id}"
|
return f"user:{user_id}"
|
||||||
|
|
||||||
def get_user(self, user_id: str) -> dict | None:
|
def get_user(self, user_id: str) -> dict[str, Any] | None:
|
||||||
return self.get(self._key(user_id))
|
return self.get(self._key(user_id))
|
||||||
|
|
||||||
def save_user(self, user_id: str, user: dict) -> None:
|
def save_user(self, user_id: str, user: dict[str, Any]) -> None:
|
||||||
self.set(self._key(user_id), user)
|
self.set(self._key(user_id), user)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "python-repositories"
|
name = "python-repositories"
|
||||||
version = "2.0.0"
|
version = "2.0.2"
|
||||||
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 = "schnitzelen@gmail.com" }
|
{ name = "Brian Bjarke Jensen", email = "schnitzelen@gmail.com" }
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ConnectionAwareAdapter(ConnectionAwareInterface, ContextAwareInterface):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.logger = structlog.get_logger(self.__class__.__name__)
|
self.logger = structlog.get_logger(self.__class__.__name__)
|
||||||
|
self._client_injected = False
|
||||||
self._health_check_at: float | None = None
|
self._health_check_at: float | None = None
|
||||||
self._health_check_ok: bool = False
|
self._health_check_ok: bool = False
|
||||||
|
|
||||||
@@ -57,6 +58,27 @@ class ConnectionAwareAdapter(ConnectionAwareInterface, ContextAwareInterface):
|
|||||||
def _probe_connection(self) -> bool:
|
def _probe_connection(self) -> bool:
|
||||||
"""Backend-specific liveness check; called only when client is ready."""
|
"""Backend-specific liveness check; called only when client is ready."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _validate_injected_client(self) -> None:
|
||||||
|
"""Verify an injected client is reachable; raise ConnectionError on failure."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _establish_connection(self) -> None:
|
||||||
|
"""Create a backend client and set internal connection state."""
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""Connect to the backend; idempotent when already connected and healthy."""
|
||||||
|
if self._client_injected:
|
||||||
|
self._validate_injected_client()
|
||||||
|
self._invalidate_health_cache()
|
||||||
|
return
|
||||||
|
if self._is_client_ready() and self.is_connected():
|
||||||
|
self.logger.info(f"Already connected to {self.connection_name}")
|
||||||
|
return
|
||||||
|
self.disconnect()
|
||||||
|
self._establish_connection()
|
||||||
|
self._invalidate_health_cache()
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
"""Check if connected to the backend."""
|
"""Check if connected to the backend."""
|
||||||
if not self._is_client_ready():
|
if not self._is_client_ready():
|
||||||
|
|||||||
@@ -58,23 +58,17 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
def _is_client_ready(self) -> bool:
|
def _is_client_ready(self) -> bool:
|
||||||
return self._client is not None and self._bucket_name is not None
|
return self._client is not None and self._bucket_name is not None
|
||||||
|
|
||||||
def connect(self) -> None:
|
def _validate_injected_client(self) -> None:
|
||||||
"""Connect to the Minio server."""
|
if self._client is None:
|
||||||
if self._client_injected:
|
|
||||||
if self._client is not None:
|
|
||||||
try:
|
|
||||||
_ = self._client.list_buckets()
|
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Could not connect to Minio at {self._config.endpoint}"
|
|
||||||
) from exc
|
|
||||||
self._invalidate_health_cache()
|
|
||||||
return
|
return
|
||||||
if self._client is not None and self.is_connected():
|
try:
|
||||||
self.logger.info("Already connected to Minio")
|
_ = self._client.list_buckets()
|
||||||
return
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
if self._client is not None:
|
raise ConnectionError(
|
||||||
self.disconnect()
|
f"Could not connect to Minio at {self._config.endpoint}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def _establish_connection(self) -> None:
|
||||||
endpoint = self._config.endpoint
|
endpoint = self._config.endpoint
|
||||||
access_key = self._config.access_key
|
access_key = self._config.access_key
|
||||||
secret_key = self._config.secret_key
|
secret_key = self._config.secret_key
|
||||||
@@ -98,7 +92,6 @@ class MinioAdapter(ObjectRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
client.make_bucket(bucket)
|
client.make_bucket(bucket)
|
||||||
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."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from typing import cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from python_repositories.adapters.connection_aware_adapter import (
|
from python_repositories.adapters.connection_aware_adapter import (
|
||||||
ConnectionAwareAdapter,
|
ConnectionAwareAdapter,
|
||||||
@@ -49,25 +49,20 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
def _is_client_ready(self) -> bool:
|
def _is_client_ready(self) -> bool:
|
||||||
return self._client is not None
|
return self._client is not None
|
||||||
|
|
||||||
def connect(self) -> None:
|
def _validate_injected_client(self) -> None:
|
||||||
"""Connect to the Redis server."""
|
if self._client is None:
|
||||||
if self._client_injected:
|
|
||||||
if self._client is not None:
|
|
||||||
try:
|
|
||||||
if not self._client.ping():
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Could not connect to Redis at {self._config.uri}"
|
|
||||||
)
|
|
||||||
except (redis.ConnectionError, redis.TimeoutError) as exc:
|
|
||||||
raise ConnectionError(
|
|
||||||
f"Could not connect to Redis at {self._config.uri}"
|
|
||||||
) from exc
|
|
||||||
self._invalidate_health_cache()
|
|
||||||
return
|
return
|
||||||
if self._client is not None:
|
try:
|
||||||
self._client.close()
|
if not self._client.ping():
|
||||||
self._client = None
|
raise ConnectionError(
|
||||||
self._invalidate_health_cache()
|
f"Could not connect to Redis at {self._config.uri}"
|
||||||
|
)
|
||||||
|
except (redis.ConnectionError, redis.TimeoutError) as exc:
|
||||||
|
raise ConnectionError(
|
||||||
|
f"Could not connect to Redis at {self._config.uri}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def _establish_connection(self) -> None:
|
||||||
uri = self._config.uri
|
uri = self._config.uri
|
||||||
try:
|
try:
|
||||||
client = redis.Redis.from_url(
|
client = redis.Redis.from_url(
|
||||||
@@ -79,7 +74,6 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
except (redis.ConnectionError, redis.TimeoutError) as exc:
|
except (redis.ConnectionError, redis.TimeoutError) as exc:
|
||||||
raise ConnectionError(f"Could not connect to Redis at {uri}") from exc
|
raise ConnectionError(f"Could not connect to Redis at {uri}") from exc
|
||||||
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."""
|
||||||
@@ -95,7 +89,7 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
except (redis.ConnectionError, redis.TimeoutError):
|
except (redis.ConnectionError, redis.TimeoutError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def set(self, key: str, data: dict) -> None:
|
def set(self, key: str, data: dict[str, Any]) -> None:
|
||||||
"""Set a JSON object in Redis."""
|
"""Set a JSON object in Redis."""
|
||||||
# Check input
|
# Check input
|
||||||
if not isinstance(key, str) or len(key) == 0:
|
if not isinstance(key, str) or len(key) == 0:
|
||||||
@@ -109,7 +103,7 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
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}")
|
||||||
|
|
||||||
def get(self, key: str) -> dict | None:
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
"""Get a JSON object from Redis."""
|
"""Get a JSON object from Redis."""
|
||||||
# Check input
|
# Check input
|
||||||
if not isinstance(key, str) or len(key) == 0:
|
if not isinstance(key, str) or len(key) == 0:
|
||||||
@@ -119,7 +113,7 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
|
|||||||
assert self._client is not None
|
assert self._client is not None
|
||||||
# Get data
|
# Get data
|
||||||
data = cast(
|
data = cast(
|
||||||
dict | None,
|
dict[str, Any] | None,
|
||||||
self._client.json().get(key),
|
self._client.json().get(key),
|
||||||
)
|
)
|
||||||
self.logger.debug(f"Got {data} from {key}")
|
self.logger.debug(f"Got {data} from {key}")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Example domain repository backed by Redis JSON."""
|
"""Example domain repository backed by Redis JSON."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from python_repositories.adapters.redis_adapter import RedisAdapter
|
from python_repositories.adapters.redis_adapter import RedisAdapter
|
||||||
|
|
||||||
|
|
||||||
@@ -9,10 +11,10 @@ class UserJsonRepository(RedisAdapter):
|
|||||||
def _key(self, user_id: str) -> str:
|
def _key(self, user_id: str) -> str:
|
||||||
return f"user:{user_id}"
|
return f"user:{user_id}"
|
||||||
|
|
||||||
def get_user(self, user_id: str) -> dict | None:
|
def get_user(self, user_id: str) -> dict[str, Any] | None:
|
||||||
return self.get(self._key(user_id))
|
return self.get(self._key(user_id))
|
||||||
|
|
||||||
def save_user(self, user_id: str, user: dict) -> None:
|
def save_user(self, user_id: str, user: dict[str, Any]) -> None:
|
||||||
self.set(self._key(user_id), user)
|
self.set(self._key(user_id), user)
|
||||||
|
|
||||||
def delete_user(self, user_id: str) -> None:
|
def delete_user(self, user_id: str) -> None:
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ class ConnectionAwareInterface(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
"""Connect to resource."""
|
"""Connect to resource.
|
||||||
|
|
||||||
|
Implementations should be idempotent: calling connect while already
|
||||||
|
connected and healthy is a no-op.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -2,18 +2,19 @@
|
|||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class JsonRepositoryInterface(ABC):
|
class JsonRepositoryInterface(ABC):
|
||||||
"""Interface that defines JSON document CRUD methods."""
|
"""Interface that defines JSON document CRUD methods."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get(self, key: str) -> dict | None:
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
"""Get a JSON object by key."""
|
"""Get a JSON object by key."""
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set(self, key: str, data: dict) -> None:
|
def set(self, key: str, data: dict[str, Any]) -> None:
|
||||||
"""Set a JSON object by key."""
|
"""Set a JSON object by key."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Integration tests for the RedisAdapter."""
|
"""Integration tests for the RedisAdapter."""
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import redis
|
import redis
|
||||||
@@ -48,6 +49,16 @@ def clear_redis(raw_redis_client: redis.Redis) -> None:
|
|||||||
raw_redis_client.flushall()
|
raw_redis_client.flushall()
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_log_info_when_already_connected(
|
||||||
|
redis_adapter: RedisAdapter,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the RedisAdapter logs info when connect is called while already connected."""
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
redis_adapter.connect()
|
||||||
|
assert "Already connected to Redis" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_should_raise_connection_error_when_unable_to_connect() -> None:
|
def test_should_raise_connection_error_when_unable_to_connect() -> None:
|
||||||
"""Test that the RedisAdapter raises ConnectionError when unable to connect."""
|
"""Test that the RedisAdapter raises ConnectionError when unable to connect."""
|
||||||
adapter = RedisAdapter(config=RedisConfig(uri="redis://invalid:6379"))
|
adapter = RedisAdapter(config=RedisConfig(uri="redis://invalid:6379"))
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Unit tests for JsonRepositoryInterface."""
|
"""Unit tests for JsonRepositoryInterface."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from python_repositories.interfaces.json_repository_interface import (
|
from python_repositories.interfaces.json_repository_interface import (
|
||||||
JsonRepositoryInterface,
|
JsonRepositoryInterface,
|
||||||
@@ -12,7 +14,7 @@ def test_instantiation_fails_when_get_not_implemented() -> None:
|
|||||||
class Incomplete(JsonRepositoryInterface):
|
class Incomplete(JsonRepositoryInterface):
|
||||||
"""A class that does not implement get."""
|
"""A class that does not implement get."""
|
||||||
|
|
||||||
def set(self, key: str, data: dict) -> None:
|
def set(self, key: str, data: dict[str, Any]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def delete(self, key: str) -> None:
|
def delete(self, key: str) -> None:
|
||||||
@@ -31,7 +33,7 @@ def test_instantiation_fails_when_set_not_implemented() -> None:
|
|||||||
class Incomplete(JsonRepositoryInterface):
|
class Incomplete(JsonRepositoryInterface):
|
||||||
"""A class that does not implement set."""
|
"""A class that does not implement set."""
|
||||||
|
|
||||||
def get(self, key: str) -> dict | None:
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete(self, key: str) -> None:
|
def delete(self, key: str) -> None:
|
||||||
@@ -50,10 +52,10 @@ def test_instantiation_fails_when_delete_not_implemented() -> None:
|
|||||||
class Incomplete(JsonRepositoryInterface):
|
class Incomplete(JsonRepositoryInterface):
|
||||||
"""A class that does not implement delete."""
|
"""A class that does not implement delete."""
|
||||||
|
|
||||||
def get(self, key: str) -> dict | None:
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set(self, key: str, data: dict) -> None:
|
def set(self, key: str, data: dict[str, Any]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def list_keys(self, pattern: str) -> list[str]:
|
def list_keys(self, pattern: str) -> list[str]:
|
||||||
@@ -69,10 +71,10 @@ def test_instantiation_fails_when_list_keys_not_implemented() -> None:
|
|||||||
class Incomplete(JsonRepositoryInterface):
|
class Incomplete(JsonRepositoryInterface):
|
||||||
"""A class that does not implement list_keys."""
|
"""A class that does not implement list_keys."""
|
||||||
|
|
||||||
def get(self, key: str) -> dict | None:
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set(self, key: str, data: dict) -> None:
|
def set(self, key: str, data: dict[str, Any]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def delete(self, key: str) -> None:
|
def delete(self, key: str) -> None:
|
||||||
@@ -86,10 +88,10 @@ def test_scan_keys_defaults_to_list_keys() -> None:
|
|||||||
"""Test that the default scan_keys implementation delegates to list_keys."""
|
"""Test that the default scan_keys implementation delegates to list_keys."""
|
||||||
|
|
||||||
class Complete(JsonRepositoryInterface):
|
class Complete(JsonRepositoryInterface):
|
||||||
def get(self, key: str) -> dict | None:
|
def get(self, key: str) -> dict[str, Any] | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set(self, key: str, data: dict) -> None:
|
def set(self, key: str, data: dict[str, Any]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def delete(self, key: str) -> None:
|
def delete(self, key: str) -> None:
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ def test_connect_with_injected_client_raises_on_failure() -> None:
|
|||||||
adapter.connect()
|
adapter.connect()
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect_with_injected_client_skips_validation_when_client_cleared() -> None:
|
||||||
|
mock_client = MagicMock(spec=Minio)
|
||||||
|
adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client)
|
||||||
|
adapter.disconnect()
|
||||||
|
|
||||||
|
adapter.connect()
|
||||||
|
|
||||||
|
mock_client.list_buckets.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_connect_disconnects_before_reconnect(
|
def test_connect_disconnects_before_reconnect(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -95,6 +105,27 @@ def test_connect_disconnects_before_reconnect(
|
|||||||
new_client.list_buckets.assert_called_once()
|
new_client.list_buckets.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect_skips_reconnect_when_already_connected(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
stale_client = MagicMock(spec=Minio)
|
||||||
|
stale_client.bucket_exists.return_value = True
|
||||||
|
adapter = MinioAdapter(config=TEST_MINIO_CONFIG)
|
||||||
|
adapter._client = stale_client
|
||||||
|
adapter._bucket_name = TEST_MINIO_CONFIG.bucket
|
||||||
|
|
||||||
|
minio_ctor = MagicMock()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"python_repositories.adapters.minio_adapter.minio.Minio",
|
||||||
|
minio_ctor,
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter.connect()
|
||||||
|
|
||||||
|
minio_ctor.assert_not_called()
|
||||||
|
assert adapter._client is stale_client
|
||||||
|
|
||||||
|
|
||||||
def test_connect_raises_when_bucket_missing(
|
def test_connect_raises_when_bucket_missing(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -99,10 +99,21 @@ def test_connect_with_injected_client_raises_on_redis_error() -> None:
|
|||||||
adapter.connect()
|
adapter.connect()
|
||||||
|
|
||||||
|
|
||||||
def test_connect_closes_existing_non_injected_client(
|
def test_connect_with_injected_client_skips_validation_when_client_cleared() -> None:
|
||||||
|
mock_client = MagicMock(spec=redis.Redis)
|
||||||
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client)
|
||||||
|
adapter.disconnect()
|
||||||
|
|
||||||
|
adapter.connect()
|
||||||
|
|
||||||
|
mock_client.ping.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect_reconnects_when_existing_client_unhealthy(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
stale_client = MagicMock(spec=redis.Redis)
|
stale_client = MagicMock(spec=redis.Redis)
|
||||||
|
stale_client.ping.side_effect = redis.ConnectionError("connection lost")
|
||||||
new_client = MagicMock(spec=redis.Redis)
|
new_client = MagicMock(spec=redis.Redis)
|
||||||
new_client.ping.return_value = True
|
new_client.ping.return_value = True
|
||||||
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
||||||
@@ -116,6 +127,24 @@ def test_connect_closes_existing_non_injected_client(
|
|||||||
assert adapter._client is new_client
|
assert adapter._client is new_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect_skips_reconnect_when_already_connected(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
stale_client = MagicMock(spec=redis.Redis)
|
||||||
|
stale_client.ping.return_value = True
|
||||||
|
adapter = RedisAdapter(config=TEST_REDIS_CONFIG)
|
||||||
|
adapter._client = stale_client
|
||||||
|
|
||||||
|
from_url = MagicMock()
|
||||||
|
monkeypatch.setattr("redis.Redis.from_url", from_url)
|
||||||
|
|
||||||
|
adapter.connect()
|
||||||
|
|
||||||
|
stale_client.close.assert_not_called()
|
||||||
|
from_url.assert_not_called()
|
||||||
|
assert adapter._client is stale_client
|
||||||
|
|
||||||
|
|
||||||
def test_scan_keys_yields_decoded_keys() -> None:
|
def test_scan_keys_yields_decoded_keys() -> None:
|
||||||
mock_client = MagicMock(spec=redis.Redis)
|
mock_client = MagicMock(spec=redis.Redis)
|
||||||
mock_client.scan_iter.return_value = iter([b"key1", b"key2"])
|
mock_client.scan_iter.return_value = iter([b"key1", b"key2"])
|
||||||
|
|||||||
@@ -1056,7 +1056,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-repositories"
|
name = "python-repositories"
|
||||||
version = "2.0.0"
|
version = "2.0.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
|
|||||||
Reference in New Issue
Block a user