Compare commits

...
6 Commits
Author SHA1 Message Date
Brian Bjarke Jensen 2eafd73743 Merge pull request 'added redis adapter' (#1) from collect-code-from-other-repos into main
Code Quality Pipeline / code-quality (push) Successful in 17s
Test Python Package / test (push) Successful in 16s
Publish Python Package / build-and-publish (push) Successful in 9s
Reviewed-on: brian/python-repositories#1
2025-09-14 20:42:50 +02:00
Brian Bjarke Jensen 1cc047d3b8 removed unused bound ports for test
Code Quality Pipeline / code-quality (pull_request) Successful in 17s
Test Python Package / test (pull_request) Successful in 16s
2025-09-14 20:40:10 +02:00
Brian Bjarke Jensen c7cc2a3655 added optional package redis
Code Quality Pipeline / code-quality (pull_request) Successful in 17s
Test Python Package / test (pull_request) Failing after 43s
2025-09-14 20:15:11 +02:00
Brian Bjarke Jensen b5d0297375 pyupgrade fixed imports
Code Quality Pipeline / code-quality (pull_request) Successful in 17s
Test Python Package / test (pull_request) Failing after 10s
2025-09-14 20:11:26 +02:00
Brian Bjarke Jensen 2fa633a44f ruff fixed formatting
Code Quality Pipeline / code-quality (pull_request) Failing after 15s
Test Python Package / test (pull_request) Failing after 10s
2025-09-14 20:09:33 +02:00
Brian Bjarke Jensen 0e47db238a fuxed mypy issues
Code Quality Pipeline / code-quality (pull_request) Failing after 14s
Test Python Package / test (pull_request) Failing after 9s
2025-09-14 20:08:30 +02:00
6 changed files with 61 additions and 60 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Install dependencies
env:
UV_LINK_MODE: copy
run: uv sync
run: uv sync --extra redis
- name: Run pytest
env:
+4
View File
@@ -53,6 +53,10 @@ show_error_codes = true # show error codes in output
explicit_package_bases = true # reduce risk of import confusion
namespace_packages = true # enable namespace packages
[[tool.mypy.overrides]]
module = "testcontainers.*"
ignore_missing_imports = true
[dependency-groups]
dev = [
"mypy>=1.17.1",
+4 -7
View File
@@ -1,7 +1,7 @@
"""Integration tests configuration."""
import os
from typing import Generator
from collections.abc import Generator
import pytest
import redis
import structlog
@@ -27,15 +27,12 @@ def configure_logging() -> None:
@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."""
# Start container
container = RedisContainer(
image="redis/redis-stack:7.2.0-v0",
port=6379,
).with_bind_ports(
container=6379,
host=6379,
)
container.start()
# Set environment variable for Redis URI
@@ -52,7 +49,7 @@ def redis_container() -> Generator[str, None, None]:
@pytest.fixture(scope="session", autouse=True)
def set_environment_variables(
redis_container: str,
) -> Generator[dict[str, str], None, None]:
) -> Generator[dict[str, str]]:
"""Set environment variables needed for tests."""
# Build environment variables dictionary
env_vars = {"REDIS_URI": redis_container}
@@ -68,7 +65,7 @@ def set_environment_variables(
@pytest.fixture(scope="session")
def raw_redis_client(redis_container: str) -> Generator[redis.Redis, None, None]:
def raw_redis_client(redis_container: str) -> Generator[redis.Redis]:
"""Provide a raw Redis client connected to the test Redis container."""
# Connect client
client = redis.Redis.from_url(
@@ -1,13 +1,17 @@
"""Integration tests for ConnectionAwareInterface."""
import pytest
from python_repositories.interfaces.connection_aware_interface import ConnectionAwareInterface
from python_repositories.interfaces.connection_aware_interface import (
ConnectionAwareInterface,
)
def test_instantiation_fails_when_connect_not_implemented() -> None:
"""Test that instantiation fails if connect is not implemented."""
class Incomplete(ConnectionAwareInterface):
"""A class that does not implement connect."""
def disconnect(self) -> None:
pass
@@ -16,13 +20,15 @@ def test_instantiation_fails_when_connect_not_implemented() -> None:
return False
with pytest.raises(TypeError):
_ = Incomplete()
_ = Incomplete() # type: ignore
def test_instantiation_fails_when_disconnect_not_implemented() -> None:
"""Test that instantiation fails if disconnect is not implemented."""
class Incomplete(ConnectionAwareInterface):
"""A class that does not implement disconnect."""
def connect(self) -> None:
pass
@@ -31,13 +37,15 @@ def test_instantiation_fails_when_disconnect_not_implemented() -> None:
return False
with pytest.raises(TypeError):
_ = Incomplete()
_ = Incomplete() # type: ignore
def test_instantiation_fails_when_is_connected_not_implemented() -> None:
"""Test that instantiation fails if is_connected is not implemented."""
class Incomplete(ConnectionAwareInterface):
"""A class that does not implement is_connected."""
def connect(self) -> None:
pass
@@ -45,15 +53,17 @@ def test_instantiation_fails_when_is_connected_not_implemented() -> None:
pass
with pytest.raises(TypeError):
_ = Incomplete()
_ = Incomplete() # type: ignore
def test_connect_raises_not_implemented_if_not_overwritten() -> None:
"""Test that connect raises NotImplementedError if not implemented."""
class Incomplete(ConnectionAwareInterface):
"""A class that does not implement connect."""
def connect(self) -> None:
super().connect()
super().connect() # type: ignore
def disconnect(self) -> None:
pass
@@ -69,13 +79,15 @@ def test_connect_raises_not_implemented_if_not_overwritten() -> None:
def test_disconnect_raises_not_implemented_if_not_overwritten() -> None:
"""Test that disconnect raises NotImplementedError if not implemented."""
class Incomplete(ConnectionAwareInterface):
"""A class that does not implement disconnect."""
def connect(self) -> None:
pass
def disconnect(self) -> None:
super().disconnect()
super().disconnect() # type: ignore
@property
def is_connected(self) -> bool:
@@ -88,8 +100,10 @@ def test_disconnect_raises_not_implemented_if_not_overwritten() -> None:
def test_is_connected_raises_not_implemented_if_not_overwritten() -> None:
"""Test that is_connected raises NotImplementedError if not implemented."""
class Incomplete(ConnectionAwareInterface):
"""A class that does not implement is_connected."""
def connect(self) -> None:
pass
@@ -98,7 +112,7 @@ def test_is_connected_raises_not_implemented_if_not_overwritten() -> None:
@property
def is_connected(self) -> bool:
return super().is_connected
return super().is_connected # type: ignore
instance = Incomplete()
with pytest.raises(NotImplementedError):
@@ -1,39 +1,47 @@
"""Integration tests for ContextAwareInterface."""
from __future__ import annotations
import pytest
from python_repositories.interfaces.context_aware_interface import ContextAwareInterface
def test_instantiation_fails_when_enter_not_implemented() -> None:
"""Test that instantiation fails if __enter__ is not implemented."""
class Incomplete(ContextAwareInterface):
"""A class that does not implement __enter__."""
def __exit__(
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
) -> None:
pass
with pytest.raises(TypeError):
_ = Incomplete()
_ = Incomplete() # type: ignore
def test_instantiation_fails_when_exit_not_implemented() -> None:
"""Test that instantiation fails if __exit__ is not implemented."""
class Incomplete(ContextAwareInterface):
"""A class that does not implement __exit__."""
def __enter__(self) -> ContextAwareInterface:
def __enter__(self) -> Incomplete:
return self
with pytest.raises(TypeError):
_ = Incomplete()
_ = Incomplete() # type: ignore
def test_enter_raises_not_implemented_if_not_overwritten() -> None:
"""Test that __enter__ raises NotImplementedError if not implemented."""
class Incomplete(ContextAwareInterface):
"""A class that does not implement __enter__."""
def __enter__(self) -> ContextAwareInterface:
super().__enter__()
def __enter__(self) -> Incomplete:
super().__enter__() # type: ignore
return self
def __exit__(
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
@@ -47,15 +55,17 @@ def test_enter_raises_not_implemented_if_not_overwritten() -> None:
def test_exit_raises_not_implemented_if_not_overwritten() -> None:
"""Test that __exit__ raises NotImplementedError if not implemented."""
class Incomplete(ContextAwareInterface):
"""A class that does not implement __exit__."""
def __enter__(self) -> ContextAwareInterface:
return self
def __exit__(
self, exc_type: type | None, exc_val: object | None, exc_tb: object | None
) -> None:
super().__exit__(exc_type, exc_val, exc_tb)
super().__exit__(exc_type, exc_val, exc_tb) # type: ignore
instance = Incomplete()
with pytest.raises(NotImplementedError):
+15 -39
View File
@@ -3,7 +3,7 @@
# allowing tests to access RedisAdapter's internal methods as needed for integration testing.
"""Integration tests for the RedisAdapter."""
from typing import Generator
from collections.abc import Generator
import pytest
import redis
from redis.commands.json.path import Path as RedisPath
@@ -11,7 +11,7 @@ from python_repositories.adapters.redis_adapter import RedisAdapter
@pytest.fixture(scope="session")
def data() -> Generator[dict[str, str], None, None]:
def data() -> Generator[dict[str, str]]:
"""Provide a sample data dictionary for tests."""
yield {"foo": "bar"}
@@ -20,7 +20,7 @@ def data() -> Generator[dict[str, str], None, None]:
def data_in_redis(
raw_redis_client: redis.Redis,
data: dict[str, str],
) -> Generator[tuple[str, dict[str, str]], None, None]:
) -> Generator[tuple[str, dict[str, str]]]:
"""Fixture to set up a known value in Redis before each test."""
key = "test_key"
path = RedisPath.root_path()
@@ -33,7 +33,7 @@ def data_in_redis(
@pytest.fixture(scope="module")
def redis_adapter(redis_container: str) -> Generator[RedisAdapter, None, None]:
def redis_adapter(redis_container: str) -> Generator[RedisAdapter]:
"""Fixture to provide a connected RedisAdapter instance."""
adapter = RedisAdapter()
adapter.connect()
@@ -48,26 +48,20 @@ def clear_redis(raw_redis_client: redis.Redis) -> None:
raw_redis_client.flushall()
def test_should_adhere_to_interface(
redis_container: str
) -> None:
def test_should_adhere_to_interface(redis_container: str) -> None:
"""Test that the RedisAdapter adheres to the expected interface."""
# Instantiation fails if interface not adhered to
_ = RedisAdapter()
def test_should_have_logger_when_instantiated(
redis_container: str
) -> None:
def test_should_have_logger_when_instantiated(redis_container: str) -> None:
"""Test that the RedisAdapter has a logger when instantiated."""
adapter = RedisAdapter()
assert hasattr(adapter, "logger")
assert adapter.logger is not None
def test_should_not_be_connected_when_instantiated(
redis_container: str
) -> None:
def test_should_not_be_connected_when_instantiated(redis_container: str) -> None:
"""Test that the RedisAdapter is not connected when instantiated."""
adapter = RedisAdapter()
assert adapter._client is None
@@ -89,7 +83,7 @@ def test_should_raise_connection_error_when_unable_to_connect(
def test_connect_raises_connection_error_when_unable_to_ping(
monkeypatch: pytest.MonkeyPatch
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test that the RedisAdapter raises ConnectionError when ping fails."""
# Set an invalid URI
@@ -98,6 +92,7 @@ def test_connect_raises_connection_error_when_unable_to_ping(
# Monkeypatch redis.Redis.from_url to return a mock client
class MockRedis:
"""A mock Redis client that simulates a failed ping."""
def ping(self) -> bool:
"""Simulate a failed ping."""
return False # Simulate failed ping
@@ -109,23 +104,6 @@ def test_connect_raises_connection_error_when_unable_to_ping(
adapter.connect()
def test_should_connect_and_disconnect(
redis_container: str,
) -> None:
"""Test that the RedisAdapter can connect and disconnect."""
# Connect
adapter = RedisAdapter()
adapter.connect()
# Assert connected
assert adapter._client is not None
assert adapter.is_connected
# Disconnect
adapter.disconnect()
# Assert disconnected
assert adapter._client is None
assert not adapter.is_connected
def test_should_log_error_on_exception_during_exit(
redis_container: str,
caplog: pytest.LogCaptureFixture,
@@ -142,9 +120,7 @@ def test_should_log_error_on_exception_during_exit(
assert "Error while exiting context" in caplog.text
def test_should_have_context_manager(
redis_container: str
) -> None:
def test_should_have_context_manager(redis_container: str) -> None:
"""Test that the RedisAdapter can be used as a context manager."""
with RedisAdapter() as adapter:
assert adapter._client is not None
@@ -183,7 +159,7 @@ def test_should_raise_value_error_on_invalid_get_key(
# Act & Assert
for key in invalid_keys:
with pytest.raises(ValueError):
redis_adapter._get(key)
redis_adapter._get(key) # type: ignore
def test_should_raise_connection_error_on_get_when_not_connected(
@@ -234,7 +210,7 @@ def test_should_raise_value_error_on_invalid_set_key(
# Act & Assert
for key in invalid_keys:
with pytest.raises(ValueError):
redis_adapter._set(key, data)
redis_adapter._set(key, data) # type: ignore
def test_should_raise_value_error_on_invalid_set_data(
@@ -247,7 +223,7 @@ def test_should_raise_value_error_on_invalid_set_data(
# Act & Assert
for data in invalid_data:
with pytest.raises(ValueError):
redis_adapter._set(key, data)
redis_adapter._set(key, data) # type: ignore
def test_should_raise_connection_error_on_set_when_not_connected(
@@ -285,7 +261,7 @@ def test_should_raise_value_error_on_invalid_delete_key(
# Act & Assert
for key in invalid_keys:
with pytest.raises(ValueError):
redis_adapter._delete(key)
redis_adapter._delete(key) # type: ignore
def test_should_raise_connection_error_on_delete_when_not_connected(
@@ -321,7 +297,7 @@ def test_should_raise_value_error_on_invalid_list_keys_pattern(
# Act & Assert
for pattern in invalid_patterns:
with pytest.raises(ValueError):
redis_adapter._list_keys(pattern)
redis_adapter._list_keys(pattern) # type: ignore
def test_should_raise_connection_error_on_list_keys_when_not_connected(