Skip to content

Commit 9487b06

Browse files
committed
chore: remove Redis dependency, add 80% coverage threshold
- Replace Redis cache with in-memory cache - Remove redis from dependencies - Add --cov-fail-under=80 to CI tests
1 parent 2629449 commit 9487b06

File tree

5 files changed

+49
-83
lines changed

5 files changed

+49
-83
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ jobs:
5353
uv venv
5454
uv pip install -e ".[dev]"
5555
56-
- name: Run tests
56+
- name: Run tests with coverage
5757
run: |
5858
source .venv/bin/activate
59-
pytest tests/ -v --cov=src --cov-report=xml --cov-report=term-missing
59+
pytest tests/ -v --cov=src --cov-report=xml --cov-report=term-missing --cov-fail-under=80
6060
env:
6161
GROQ_API_KEY: "test-key-for-ci"
6262

Dockerfile.api

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ RUN pip install --no-cache-dir \
2929
"uvicorn[standard]>=0.30.0" \
3030
pydantic>=2.7.0 \
3131
pydantic-settings>=2.3.0 \
32-
redis>=5.0.0 \
3332
slowapi>=0.1.9 \
3433
httpx>=0.27.0 \
3534
tenacity>=8.4.0 \

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ dependencies = [
4141
"pydantic>=2.7.0",
4242
"pydantic-settings>=2.3.0",
4343

44-
# Caching & Rate Limiting
45-
"redis>=5.0.0",
44+
# Rate Limiting
4645
"slowapi>=0.1.9",
4746

4847
# Utilities

src/config/settings.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ def validate_llm_config(self) -> "Settings":
6363
qdrant_api_key: SecretStr | None = None
6464
qdrant_collection: str = "sec_filings"
6565

66-
# Redis
67-
redis_url: str = "redis://localhost:6379"
66+
# Cache
6867
cache_ttl_seconds: int = 3600 # 1 hour default
6968

7069
# Companies House (UK)

src/utils/cache.py

Lines changed: 45 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,29 @@
1-
"""Redis caching utilities for API responses."""
1+
"""In-memory caching utilities for API responses."""
22

33
import hashlib
44
import json
5+
import time
56
from functools import lru_cache
67
from typing import Any
78

8-
import redis
99
import structlog
1010

1111
from src.config import get_settings
1212

1313
logger = structlog.get_logger()
1414

1515

16-
class RedisCache:
17-
"""Redis-based caching for API responses."""
16+
class MemoryCache:
17+
"""Simple in-memory caching for API responses."""
1818

19-
def __init__(self, url: str, default_ttl: int = 3600) -> None:
20-
"""Initialize Redis cache.
19+
def __init__(self, default_ttl: int = 3600) -> None:
20+
"""Initialize memory cache.
2121
2222
Args:
23-
url: Redis connection URL
2423
default_ttl: Default TTL in seconds
2524
"""
26-
self._client: redis.Redis | None = None
27-
self._url = url
25+
self._cache: dict[str, tuple[Any, float]] = {}
2826
self._default_ttl = default_ttl
29-
self._connected = False
30-
31-
def _connect(self) -> redis.Redis | None:
32-
"""Establish Redis connection with error handling."""
33-
if self._client is not None:
34-
return self._client
35-
36-
try:
37-
self._client = redis.from_url(
38-
self._url,
39-
decode_responses=True,
40-
socket_connect_timeout=5,
41-
)
42-
# Test connection
43-
self._client.ping()
44-
self._connected = True
45-
logger.info("redis_connected", url=self._url)
46-
return self._client
47-
except redis.ConnectionError as e:
48-
logger.warning("redis_connection_failed", error=str(e))
49-
self._connected = False
50-
return None
5127

5228
@staticmethod
5329
def _make_key(prefix: str, *args: Any, **kwargs: Any) -> str:
@@ -56,53 +32,51 @@ def _make_key(prefix: str, *args: Any, **kwargs: Any) -> str:
5632
key_hash = hashlib.sha256(key_data.encode()).hexdigest()[:16]
5733
return f"{prefix}:{key_hash}"
5834

35+
def _cleanup_expired(self) -> None:
36+
"""Remove expired entries from cache."""
37+
now = time.time()
38+
expired = [k for k, (_, exp) in self._cache.items() if exp < now]
39+
for key in expired:
40+
del self._cache[key]
41+
5942
def get(self, key: str) -> Any | None:
6043
"""Get value from cache.
6144
6245
Args:
6346
key: Cache key
6447
6548
Returns:
66-
Cached value or None if not found
49+
Cached value or None if not found or expired
6750
"""
68-
client = self._connect()
69-
if client is None:
70-
return None
51+
self._cleanup_expired()
7152

72-
try:
73-
value = client.get(key)
74-
if value:
53+
if key in self._cache:
54+
value, expiry = self._cache[key]
55+
if expiry > time.time():
7556
logger.debug("cache_hit", key=key)
76-
return json.loads(value)
77-
logger.debug("cache_miss", key=key)
78-
return None
79-
except (redis.RedisError, json.JSONDecodeError) as e:
80-
logger.warning("cache_get_error", key=key, error=str(e))
81-
return None
57+
return value
58+
else:
59+
del self._cache[key]
60+
61+
logger.debug("cache_miss", key=key)
62+
return None
8263

8364
def set(self, key: str, value: Any, ttl: int | None = None) -> bool:
8465
"""Set value in cache.
8566
8667
Args:
8768
key: Cache key
88-
value: Value to cache (must be JSON serializable)
69+
value: Value to cache
8970
ttl: Time to live in seconds (uses default if not specified)
9071
9172
Returns:
92-
True if successful, False otherwise
73+
True if successful
9374
"""
94-
client = self._connect()
95-
if client is None:
96-
return False
97-
98-
try:
99-
ttl = ttl or self._default_ttl
100-
client.setex(key, ttl, json.dumps(value))
101-
logger.debug("cache_set", key=key, ttl=ttl)
102-
return True
103-
except (redis.RedisError, TypeError) as e:
104-
logger.warning("cache_set_error", key=key, error=str(e))
105-
return False
75+
ttl = ttl or self._default_ttl
76+
expiry = time.time() + ttl
77+
self._cache[key] = (value, expiry)
78+
logger.debug("cache_set", key=key, ttl=ttl)
79+
return True
10680

10781
def delete(self, key: str) -> bool:
10882
"""Delete value from cache.
@@ -111,31 +85,26 @@ def delete(self, key: str) -> bool:
11185
key: Cache key
11286
11387
Returns:
114-
True if deleted, False otherwise
88+
True if deleted, False if not found
11589
"""
116-
client = self._connect()
117-
if client is None:
118-
return False
119-
120-
try:
121-
client.delete(key)
90+
if key in self._cache:
91+
del self._cache[key]
12292
logger.debug("cache_delete", key=key)
12393
return True
124-
except redis.RedisError as e:
125-
logger.warning("cache_delete_error", key=key, error=str(e))
126-
return False
94+
return False
12795

12896
@property
12997
def is_connected(self) -> bool:
130-
"""Check if Redis is connected."""
131-
return self._connected
98+
"""Always connected for memory cache."""
99+
return True
100+
101+
102+
# Backwards compatibility aliases
103+
RedisCache = MemoryCache
132104

133105

134106
@lru_cache
135-
def get_cache() -> RedisCache:
136-
"""Get cached Redis cache instance."""
107+
def get_cache() -> MemoryCache:
108+
"""Get cached memory cache instance."""
137109
settings = get_settings()
138-
return RedisCache(
139-
url=settings.redis_url,
140-
default_ttl=settings.cache_ttl_seconds,
141-
)
110+
return MemoryCache(default_ttl=settings.cache_ttl_seconds)

0 commit comments

Comments
 (0)