From cc8f44dbe5ec69b8d1d8838cb3350c6cf8bb1be8 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 23 Sep 2025 08:16:13 +0200 Subject: [PATCH 1/4] Updated cache factory to support all cache types --- src/cache/cache_factory.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/cache/cache_factory.py b/src/cache/cache_factory.py index adcdb1e6..c3292f58 100644 --- a/src/cache/cache_factory.py +++ b/src/cache/cache_factory.py @@ -4,6 +4,9 @@ from models.config import ConversationCacheConfiguration from cache.cache import Cache from cache.noop_cache import NoopCache +from cache.in_memory_cache import InMemoryCache +from cache.postgres_cache import PostgresCache +from cache.sqlite_cache import SQLiteCache from log import get_logger logger = get_logger("cache.cache_factory") @@ -25,11 +28,17 @@ def conversation_cache(config: ConversationCacheConfiguration) -> Cache: case constants.CACHE_TYPE_NOOP: return NoopCache() case constants.CACHE_TYPE_MEMORY: - return NoopCache() + if config.memory is not None: + return InMemoryCache(config.memory) + raise ValueError("Expecting configuration for in-memory cache") case constants.CACHE_TYPE_SQLITE: - return NoopCache() + if config.sqlite is not None: + return SQLiteCache(config.sqlite) + raise ValueError("Expecting configuration for SQLite cache") case constants.CACHE_TYPE_POSTGRES: - return NoopCache() + if config.postgres is not None: + return PostgresCache(config.postgres) + raise ValueError("Expecting configuration for PostgreSQL cache") case _: raise ValueError( f"Invalid cache type: {config.type}. " From 676792feace2ebc9d0f90731506dc0d0689bb256 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 23 Sep 2025 08:16:35 +0200 Subject: [PATCH 2/4] Stub for all cache types --- src/cache/in_memory_cache.py | 108 +++++++++++++++++++++++++++++++++++ src/cache/postgres_cache.py | 108 +++++++++++++++++++++++++++++++++++ src/cache/sqlite_cache.py | 108 +++++++++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 src/cache/in_memory_cache.py create mode 100644 src/cache/postgres_cache.py create mode 100644 src/cache/sqlite_cache.py diff --git a/src/cache/in_memory_cache.py b/src/cache/in_memory_cache.py new file mode 100644 index 00000000..7c29bd2a --- /dev/null +++ b/src/cache/in_memory_cache.py @@ -0,0 +1,108 @@ +"""In-memory cache implementation.""" + +from cache.cache import Cache +from models.cache_entry import CacheEntry +from models.config import InMemoryCacheConfig +from log import get_logger +from utils.connection_decorator import connection + +logger = get_logger("cache.in_memory_cache") + + +class InMemoryCache(Cache): + """In-memory cache implementation.""" + + def __init__(self, config: InMemoryCacheConfig) -> None: + """Create a new instance of in-memory cache.""" + self.cache_config = config + + def connect(self) -> None: + """Initialize connection to database.""" + logger.info("Connecting to storage") + + def connected(self) -> bool: + """Check if connection to cache is alive.""" + return True + + def initialize_cache(self) -> None: + """Initialize cache.""" + + @connection + def get( + self, user_id: str, conversation_id: str, skip_user_id_check: bool = False + ) -> list[CacheEntry]: + """Get the value associated with the given key. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + skip_user_id_check: Skip user_id suid check. + + Returns: + Empty list. + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + return [] + + @connection + def insert_or_append( + self, + user_id: str, + conversation_id: str, + cache_entry: CacheEntry, + skip_user_id_check: bool = False, + ) -> None: + """Set the value associated with the given key. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + cache_entry: The `CacheEntry` object to store. + skip_user_id_check: Skip user_id suid check. + + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + + @connection + def delete( + self, user_id: str, conversation_id: str, skip_user_id_check: bool = False + ) -> bool: + """Delete conversation history for a given user_id and conversation_id. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + skip_user_id_check: Skip user_id suid check. + + Returns: + bool: True in all cases. + + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + return True + + @connection + def list(self, user_id: str, skip_user_id_check: bool = False) -> list[str]: + """List all conversations for a given user_id. + + Args: + user_id: User identification. + skip_user_id_check: Skip user_id suid check. + + Returns: + An empty list. + + """ + super()._check_user_id(user_id, skip_user_id_check) + return [] + + def ready(self) -> bool: + """Check if the cache is ready. + + Returns: + True in all cases. + """ + return True diff --git a/src/cache/postgres_cache.py b/src/cache/postgres_cache.py new file mode 100644 index 00000000..2ab635c3 --- /dev/null +++ b/src/cache/postgres_cache.py @@ -0,0 +1,108 @@ +"""PostgreSQL cache implementation.""" + +from cache.cache import Cache +from models.cache_entry import CacheEntry +from models.config import PostgreSQLDatabaseConfiguration +from log import get_logger +from utils.connection_decorator import connection + +logger = get_logger("cache.postgres_cache") + + +class PostgresCache(Cache): + """PostgreSQL cache implementation.""" + + def __init__(self, config: PostgreSQLDatabaseConfiguration) -> None: + """Create a new instance of PostgreSQL cache.""" + self.postgres_config = config + + def connect(self) -> None: + """Initialize connection to database.""" + logger.info("Connecting to storage") + + def connected(self) -> bool: + """Check if connection to cache is alive.""" + return True + + def initialize_cache(self) -> None: + """Initialize cache.""" + + @connection + def get( + self, user_id: str, conversation_id: str, skip_user_id_check: bool = False + ) -> list[CacheEntry]: + """Get the value associated with the given key. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + skip_user_id_check: Skip user_id suid check. + + Returns: + Empty list. + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + return [] + + @connection + def insert_or_append( + self, + user_id: str, + conversation_id: str, + cache_entry: CacheEntry, + skip_user_id_check: bool = False, + ) -> None: + """Set the value associated with the given key. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + cache_entry: The `CacheEntry` object to store. + skip_user_id_check: Skip user_id suid check. + + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + + @connection + def delete( + self, user_id: str, conversation_id: str, skip_user_id_check: bool = False + ) -> bool: + """Delete conversation history for a given user_id and conversation_id. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + skip_user_id_check: Skip user_id suid check. + + Returns: + bool: True in all cases. + + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + return True + + @connection + def list(self, user_id: str, skip_user_id_check: bool = False) -> list[str]: + """List all conversations for a given user_id. + + Args: + user_id: User identification. + skip_user_id_check: Skip user_id suid check. + + Returns: + An empty list. + + """ + super()._check_user_id(user_id, skip_user_id_check) + return [] + + def ready(self) -> bool: + """Check if the cache is ready. + + Returns: + True in all cases. + """ + return True diff --git a/src/cache/sqlite_cache.py b/src/cache/sqlite_cache.py new file mode 100644 index 00000000..59ad2428 --- /dev/null +++ b/src/cache/sqlite_cache.py @@ -0,0 +1,108 @@ +"""Cache that uses SQLite to store cached values.""" + +from cache.cache import Cache +from models.cache_entry import CacheEntry +from models.config import SQLiteDatabaseConfiguration +from log import get_logger +from utils.connection_decorator import connection + +logger = get_logger("cache.sqlite_cache") + + +class SQLiteCache(Cache): + """Cache that uses SQLite to store cached values.""" + + def __init__(self, config: SQLiteDatabaseConfiguration) -> None: + """Create a new instance of SQLite cache.""" + self.sqlite_config = config + + def connect(self) -> None: + """Initialize connection to database.""" + logger.info("Connecting to storage") + + def connected(self) -> bool: + """Check if connection to cache is alive.""" + return True + + def initialize_cache(self) -> None: + """Initialize cache.""" + + @connection + def get( + self, user_id: str, conversation_id: str, skip_user_id_check: bool = False + ) -> list[CacheEntry]: + """Get the value associated with the given key. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + skip_user_id_check: Skip user_id suid check. + + Returns: + Empty list. + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + return [] + + @connection + def insert_or_append( + self, + user_id: str, + conversation_id: str, + cache_entry: CacheEntry, + skip_user_id_check: bool = False, + ) -> None: + """Set the value associated with the given key. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + cache_entry: The `CacheEntry` object to store. + skip_user_id_check: Skip user_id suid check. + + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + + @connection + def delete( + self, user_id: str, conversation_id: str, skip_user_id_check: bool = False + ) -> bool: + """Delete conversation history for a given user_id and conversation_id. + + Args: + user_id: User identification. + conversation_id: Conversation ID unique for given user. + skip_user_id_check: Skip user_id suid check. + + Returns: + bool: True in all cases. + + """ + # just check if user_id and conversation_id are UUIDs + super().construct_key(user_id, conversation_id, skip_user_id_check) + return True + + @connection + def list(self, user_id: str, skip_user_id_check: bool = False) -> list[str]: + """List all conversations for a given user_id. + + Args: + user_id: User identification. + skip_user_id_check: Skip user_id suid check. + + Returns: + An empty list. + + """ + super()._check_user_id(user_id, skip_user_id_check) + return [] + + def ready(self) -> bool: + """Check if the cache is ready. + + Returns: + True in all cases. + """ + return True From d82d6133cd9b79f7b695a6e8c156d309ae0a66eb Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 23 Sep 2025 08:16:51 +0200 Subject: [PATCH 3/4] Updated unit tests accordingly --- tests/unit/cache/test_cache_factory.py | 106 ++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/tests/unit/cache/test_cache_factory.py b/tests/unit/cache/test_cache_factory.py index 40f9492a..8b575505 100644 --- a/tests/unit/cache/test_cache_factory.py +++ b/tests/unit/cache/test_cache_factory.py @@ -2,10 +2,25 @@ import pytest -from constants import CACHE_TYPE_NOOP -from models.config import ConversationCacheConfiguration +from constants import ( + CACHE_TYPE_NOOP, + CACHE_TYPE_MEMORY, + CACHE_TYPE_SQLITE, + CACHE_TYPE_POSTGRES, +) + +from models.config import ( + ConversationCacheConfiguration, + InMemoryCacheConfig, + SQLiteDatabaseConfiguration, + PostgreSQLDatabaseConfiguration, +) + from cache.cache_factory import CacheFactory from cache.noop_cache import NoopCache +from cache.in_memory_cache import InMemoryCache +from cache.sqlite_cache import SQLiteCache +from cache.postgres_cache import PostgresCache @pytest.fixture(scope="module", name="noop_cache_config_fixture") @@ -14,6 +29,33 @@ def noop_cache_config(): return ConversationCacheConfiguration(type=CACHE_TYPE_NOOP) +@pytest.fixture(scope="module", name="memory_cache_config_fixture") +def memory_cache_config(): + """Fixture containing initialized instance of InMemory cache.""" + return ConversationCacheConfiguration( + type=CACHE_TYPE_MEMORY, memory=InMemoryCacheConfig(max_entries=10) + ) + + +@pytest.fixture(scope="module", name="postgres_cache_config_fixture") +def postgres_cache_config(): + """Fixture containing initialized instance of PostgreSQL cache.""" + return ConversationCacheConfiguration( + type=CACHE_TYPE_POSTGRES, + postgres=PostgreSQLDatabaseConfiguration( + db="database", user="user", password="password" + ), + ) + + +@pytest.fixture(scope="module", name="sqlite_cache_config_fixture") +def sqlite_cache_config(): + """Fixture containing initialized instance of SQLite cache.""" + return ConversationCacheConfiguration( + type=CACHE_TYPE_SQLITE, sqlite=SQLiteDatabaseConfiguration(db_path="foo") + ) + + @pytest.fixture(scope="module", name="invalid_cache_type_config_fixture") def invalid_cache_type_config(): """Fixture containing instance of ConversationCacheConfiguration with improper settings.""" @@ -30,6 +72,66 @@ def test_conversation_cache_noop(noop_cache_config_fixture): assert isinstance(cache, NoopCache) +def test_conversation_cache_in_memory(memory_cache_config_fixture): + """Check if InMemoryCache is returned by factory with proper configuration.""" + cache = CacheFactory.conversation_cache(memory_cache_config_fixture) + assert cache is not None + # check if the object has the right type + assert isinstance(cache, InMemoryCache) + + +def test_conversation_cache_in_memory_improper_config(): + """Check if memory cache configuration is checked in cache factory.""" + cc = ConversationCacheConfiguration( + type=CACHE_TYPE_MEMORY, memory=InMemoryCacheConfig(max_entries=10) + ) + # simulate improper configuration (can not be done directly as model checks this) + cc.memory = None + with pytest.raises(ValueError, match="Expecting configuration for in-memory cache"): + _ = CacheFactory.conversation_cache(cc) + + +def test_conversation_cache_sqlite(sqlite_cache_config_fixture): + """Check if SQLiteCache is returned by factory with proper configuration.""" + cache = CacheFactory.conversation_cache(sqlite_cache_config_fixture) + assert cache is not None + # check if the object has the right type + assert isinstance(cache, SQLiteCache) + + +def test_conversation_cache_sqlite_improper_config(): + """Check if memory cache configuration is checked in cache factory.""" + cc = ConversationCacheConfiguration( + type=CACHE_TYPE_SQLITE, sqlite=SQLiteDatabaseConfiguration(db_path="foo") + ) + # simulate improper configuration (can not be done directly as model checks this) + cc.sqlite = None + with pytest.raises(ValueError, match="Expecting configuration for SQLite cache"): + _ = CacheFactory.conversation_cache(cc) + + +def test_conversation_cache_postgres(postgres_cache_config_fixture): + """Check if PostgreSQL is returned by factory with proper configuration.""" + cache = CacheFactory.conversation_cache(postgres_cache_config_fixture) + assert cache is not None + # check if the object has the right type + assert isinstance(cache, PostgresCache) + + +def test_conversation_cache_postgres_improper_config(): + """Check if PostgreSQL cache configuration is checked in cache factory.""" + cc = ConversationCacheConfiguration( + type=CACHE_TYPE_POSTGRES, + postgres=PostgreSQLDatabaseConfiguration(db="db", user="u", password="p"), + ) + # simulate improper configuration (can not be done directly as model checks this) + cc.postgres = None + with pytest.raises( + ValueError, match="Expecting configuration for PostgreSQL cache" + ): + _ = CacheFactory.conversation_cache(cc) + + def test_conversation_cache_wrong_cache(invalid_cache_type_config_fixture): """Check if wrong cache configuration is detected properly.""" with pytest.raises(ValueError, match="Invalid cache type"): From f536842476954f83f900fe8a0a5cd3f698481ce3 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Tue, 23 Sep 2025 08:37:27 +0200 Subject: [PATCH 4/4] Updated fro catching no type error --- src/cache/cache_factory.py | 6 ++++-- tests/unit/cache/test_cache_factory.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/cache/cache_factory.py b/src/cache/cache_factory.py index c3292f58..de50edb0 100644 --- a/src/cache/cache_factory.py +++ b/src/cache/cache_factory.py @@ -39,9 +39,11 @@ def conversation_cache(config: ConversationCacheConfiguration) -> Cache: if config.postgres is not None: return PostgresCache(config.postgres) raise ValueError("Expecting configuration for PostgreSQL cache") + case None: + raise ValueError("Cache type must be set") case _: raise ValueError( f"Invalid cache type: {config.type}. " - f"Use '{constants.CACHE_TYPE_POSTGRES}' '{constants.CACHE_TYPE_SQLITE}' or " - f"'{constants.CACHE_TYPE_MEMORY}' options." + f"Use '{constants.CACHE_TYPE_POSTGRES}' '{constants.CACHE_TYPE_SQLITE}' " + f"'{constants.CACHE_TYPE_MEMORY} or {constants.CACHE_TYPE_NOOP}' options." ) diff --git a/tests/unit/cache/test_cache_factory.py b/tests/unit/cache/test_cache_factory.py index 8b575505..c9f48c77 100644 --- a/tests/unit/cache/test_cache_factory.py +++ b/tests/unit/cache/test_cache_factory.py @@ -132,6 +132,15 @@ def test_conversation_cache_postgres_improper_config(): _ = CacheFactory.conversation_cache(cc) +def test_conversation_cache_no_type(): + """Check if wrong cache configuration is detected properly.""" + cc = ConversationCacheConfiguration(type=CACHE_TYPE_NOOP) + # simulate improper configuration (can not be done directly as model checks this) + cc.type = None + with pytest.raises(ValueError, match="Cache type must be set"): + CacheFactory.conversation_cache(cc) + + def test_conversation_cache_wrong_cache(invalid_cache_type_config_fixture): """Check if wrong cache configuration is detected properly.""" with pytest.raises(ValueError, match="Invalid cache type"):