Skip to content

Commit b39c35f

Browse files
authored
Merge pull request #136 from ayushchoudhary01/feat/markets-endpoint-cache
feat(api): cache markets endpoint for 5 minutes
2 parents e22d935 + 3839697 commit b39c35f

4 files changed

Lines changed: 110 additions & 21 deletions

File tree

backend/main.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from slowapi.errors import RateLimitExceeded
1212
from slowapi.middleware import SlowAPIMiddleware
1313
from rate_limiter import limiter
14+
from fastapi_cache import FastAPICache
15+
from fastapi_cache.backends.inmemory import InMemoryBackend
16+
from fastapi_cache.decorator import cache
1417
from chat_router import router as chat_router
1518

1619

@@ -95,6 +98,7 @@ async def lifespan(app: FastAPI):
9598
f"WARNING: Model files not found at {MODEL_DIR}. "
9699
"Scan endpoints will return 503 until models are present."
97100
)
101+
FastAPICache.init(InMemoryBackend(), prefix="freshscanai-cache")
98102
yield
99103

100104

@@ -771,31 +775,35 @@ async def get_vendor_leaderboard():
771775
# ── MAP ───────────────────────────────────────────────────────────────────────
772776

773777

778+
@cache(expire=300, namespace="markets")
779+
async def _get_markets_cached() -> dict:
780+
resp = (
781+
_db()
782+
.table("vendors")
783+
.select("id, name, avg_freshness_score, trust_score, lat, lng, vendor_count")
784+
.execute()
785+
)
786+
markets = [
787+
{
788+
"id": i + 1,
789+
"name": v["name"],
790+
"score": int(v.get("avg_freshness_score") or v.get("trust_score") or 0),
791+
"lat": float(v.get("lat") or 0),
792+
"lng": float(v.get("lng") or 0),
793+
"vendors": int(v.get("vendor_count") or 1),
794+
}
795+
for i, v in enumerate(resp.data or [])
796+
if v.get("lat") and v.get("lng")
797+
]
798+
return {"success": True, "markets": markets}
799+
800+
774801
@app.get("/api/v1/maps/markets")
775802
@limiter.limit("20/minute")
776803
async def get_markets(request: Request):
777804
try:
778-
resp = (
779-
_db()
780-
.table("vendors")
781-
.select("id, name, avg_freshness_score, trust_score, lat, lng, vendor_count")
782-
.execute()
783-
)
784-
markets = [
785-
{
786-
"id": i + 1,
787-
"name": v["name"],
788-
"score": int(v.get("avg_freshness_score") or v.get("trust_score") or 0),
789-
"lat": float(v.get("lat") or 0),
790-
"lng": float(v.get("lng") or 0),
791-
"vendors": int(v.get("vendor_count") or 1),
792-
}
793-
for i, v in enumerate(resp.data or [])
794-
if v.get("lat") and v.get("lng")
795-
]
796-
return {"success": True, "markets": markets}
805+
return await _get_markets_cached()
797806
except Exception:
798-
# Migration not applied yet — return empty markets, map still renders
799807
return {
800808
"success": True,
801809
"markets": [],

backend/requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ pytest>=8.0.0
1111
# Comment these out if you don't have GPU/model files and just want demo mode.
1212
torch>=2.2.0
1313
torchvision>=0.27.0
14-
slowapi==0.1.9 # rate limiting for FastAPI; added for per-user scan endpoint throttling
14+
slowapi==0.1.9 # rate limiting for FastAPI; added for per-user scan endpoint throttling
15+
16+
fastapi-cache2==0.2.2

backend/tests/test_cache.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import asyncio
2+
3+
import pytest
4+
from fastapi_cache import FastAPICache
5+
from fastapi_cache.backends.inmemory import InMemoryBackend
6+
from fastapi_cache.decorator import cache
7+
8+
9+
@pytest.fixture(autouse=True)
10+
def _init_cache():
11+
FastAPICache.init(InMemoryBackend(), prefix="test-cache")
12+
yield
13+
14+
15+
def test_cache_returns_same_value_within_ttl():
16+
calls = {"count": 0}
17+
18+
@cache(expire=60, namespace="test_ns_1")
19+
async def handler():
20+
calls["count"] += 1
21+
return {"value": calls["count"]}
22+
23+
first = asyncio.run(handler())
24+
second = asyncio.run(handler())
25+
26+
assert first == {"value": 1}
27+
assert second == {"value": 1}
28+
assert calls["count"] == 1
29+
30+
31+
def test_cache_expires_after_ttl():
32+
calls = {"count": 0}
33+
34+
@cache(expire=1, namespace="test_ns_2")
35+
async def handler():
36+
calls["count"] += 1
37+
return {"value": calls["count"]}
38+
39+
asyncio.run(handler())
40+
asyncio.run(asyncio.sleep(2.1))
41+
second = asyncio.run(handler())
42+
43+
assert second == {"value": 2}
44+
assert calls["count"] == 2
45+
46+
47+
def test_cache_clear_forces_recompute():
48+
calls = {"count": 0}
49+
50+
@cache(expire=60, namespace="test_ns_3")
51+
async def handler():
52+
calls["count"] += 1
53+
return {"value": calls["count"]}
54+
55+
asyncio.run(handler())
56+
asyncio.run(FastAPICache.clear(namespace="test_ns_3"))
57+
second = asyncio.run(handler())
58+
59+
assert second == {"value": 2}
60+
assert calls["count"] == 2
61+
62+
63+
def test_exception_inside_cached_function_is_not_cached():
64+
calls = {"count": 0}
65+
66+
@cache(expire=60, namespace="test_ns_4")
67+
async def handler():
68+
calls["count"] += 1
69+
raise ValueError("boom")
70+
71+
with pytest.raises(ValueError):
72+
asyncio.run(handler())
73+
with pytest.raises(ValueError):
74+
asyncio.run(handler())
75+
76+
assert calls["count"] == 2

backend/vendors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import APIRouter, HTTPException, Depends, Query
22
from datetime import datetime, timedelta, timezone
33
from auth import get_current_user
4+
from fastapi_cache import FastAPICache
45

56
router = APIRouter(prefix="/api/v1/vendors", tags=["vendors"])
67

@@ -128,6 +129,8 @@ async def recalculate_trust_score(
128129
}
129130
).eq("id", vendor_id).execute()
130131

132+
await FastAPICache.clear(namespace="markets")
133+
131134
return {
132135
"success": True,
133136
"vendor_id": vendor_id,

0 commit comments

Comments
 (0)