Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from rate_limiter import limiter
from fastapi_cache import FastAPICache
from fastapi_cache.backends.inmemory import InMemoryBackend
from fastapi_cache.decorator import cache
from chat_router import router as chat_router


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


Expand Down Expand Up @@ -771,31 +775,35 @@ async def get_vendor_leaderboard():
# ── MAP ───────────────────────────────────────────────────────────────────────


@cache(expire=300, namespace="markets")
async def _get_markets_cached() -> dict:
resp = (
_db()
.table("vendors")
.select("id, name, avg_freshness_score, trust_score, lat, lng, vendor_count")
.execute()
)
markets = [
{
"id": i + 1,
"name": v["name"],
"score": int(v.get("avg_freshness_score") or v.get("trust_score") or 0),
"lat": float(v.get("lat") or 0),
"lng": float(v.get("lng") or 0),
"vendors": int(v.get("vendor_count") or 1),
}
for i, v in enumerate(resp.data or [])
if v.get("lat") and v.get("lng")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Zero coordinates are incorrectly filtered out.

Line 793 uses truthiness checks, so valid 0 latitude/longitude values are excluded. Use explicit None checks.

Proposed fix
-        if v.get("lat") and v.get("lng")
+        if v.get("lat") is not None and v.get("lng") is not None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if v.get("lat") and v.get("lng")
if v.get("lat") is not None and v.get("lng") is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` at line 793, The condition checking v.get("lat") and
v.get("lng") uses truthiness evaluation which incorrectly filters out valid
coordinates when the value is 0, since 0 is falsy in Python. Change the
condition to explicitly check whether the values are not None instead of relying
on truthiness, so that coordinates with value 0 are properly included.

]
return {"success": True, "markets": markets}


@app.get("/api/v1/maps/markets")
@limiter.limit("20/minute")
async def get_markets(request: Request):
try:
resp = (
_db()
.table("vendors")
.select("id, name, avg_freshness_score, trust_score, lat, lng, vendor_count")
.execute()
)
markets = [
{
"id": i + 1,
"name": v["name"],
"score": int(v.get("avg_freshness_score") or v.get("trust_score") or 0),
"lat": float(v.get("lat") or 0),
"lng": float(v.get("lng") or 0),
"vendors": int(v.get("vendor_count") or 1),
}
for i, v in enumerate(resp.data or [])
if v.get("lat") and v.get("lng")
]
return {"success": True, "markets": markets}
return await _get_markets_cached()
except Exception:
# Migration not applied yet — return empty markets, map still renders
return {
"success": True,
"markets": [],
Expand Down
4 changes: 3 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ pytest>=8.0.0
# Comment these out if you don't have GPU/model files and just want demo mode.
torch>=2.2.0
torchvision>=0.27.0
slowapi==0.1.9 # rate limiting for FastAPI; added for per-user scan endpoint throttling
slowapi==0.1.9 # rate limiting for FastAPI; added for per-user scan endpoint throttling

fastapi-cache2==0.2.2
76 changes: 76 additions & 0 deletions backend/tests/test_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import asyncio

import pytest
from fastapi_cache import FastAPICache
from fastapi_cache.backends.inmemory import InMemoryBackend
from fastapi_cache.decorator import cache


@pytest.fixture(autouse=True)
def _init_cache():
FastAPICache.init(InMemoryBackend(), prefix="test-cache")
yield


def test_cache_returns_same_value_within_ttl():
calls = {"count": 0}

@cache(expire=60, namespace="test_ns_1")
async def handler():
calls["count"] += 1
return {"value": calls["count"]}

first = asyncio.run(handler())
second = asyncio.run(handler())

assert first == {"value": 1}
assert second == {"value": 1}
assert calls["count"] == 1


def test_cache_expires_after_ttl():
calls = {"count": 0}

@cache(expire=1, namespace="test_ns_2")
async def handler():
calls["count"] += 1
return {"value": calls["count"]}

asyncio.run(handler())
asyncio.run(asyncio.sleep(2.1))
second = asyncio.run(handler())

assert second == {"value": 2}
assert calls["count"] == 2


def test_cache_clear_forces_recompute():
calls = {"count": 0}

@cache(expire=60, namespace="test_ns_3")
async def handler():
calls["count"] += 1
return {"value": calls["count"]}

asyncio.run(handler())
asyncio.run(FastAPICache.clear(namespace="test_ns_3"))
second = asyncio.run(handler())

assert second == {"value": 2}
assert calls["count"] == 2


def test_exception_inside_cached_function_is_not_cached():
calls = {"count": 0}

@cache(expire=60, namespace="test_ns_4")
async def handler():
calls["count"] += 1
raise ValueError("boom")

with pytest.raises(ValueError):
asyncio.run(handler())
with pytest.raises(ValueError):
asyncio.run(handler())

assert calls["count"] == 2
3 changes: 3 additions & 0 deletions backend/vendors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter, HTTPException, Depends, Query
from datetime import datetime, timedelta, timezone
from auth import get_current_user
from fastapi_cache import FastAPICache

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

Expand Down Expand Up @@ -128,6 +129,8 @@ async def recalculate_trust_score(
}
).eq("id", vendor_id).execute()

await FastAPICache.clear(namespace="markets")

Comment on lines +132 to +133

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, explore the file structure and understand the context
fd -t f "vendors.py" backend/

# Check file size
wc -l backend/vendors.py

# Map the file structure with ast-grep
ast-grep outline backend/vendors.py --view compact

Repository: jpdevhub/FreshScanAi

Length of output: 375


🏁 Script executed:

# Read the context around lines 132-133
sed -n '120,145p' backend/vendors.py

# Also map the file structure with correct view option
ast-grep outline backend/vendors.py --view expanded

Repository: jpdevhub/FreshScanAi

Length of output: 1278


🏁 Script executed:

# Get the full function to see the complete picture
sed -n '60,145p' backend/vendors.py

Repository: jpdevhub/FreshScanAi

Length of output: 3413


🏁 Script executed:

# Search for other cache clear patterns in the codebase
rg "FastAPICache.clear" backend/ -A 2 -B 2

# Check if there are other cache operations that might have error handling
rg "FastAPICache" backend/ -A 2 -B 2

Repository: jpdevhub/FreshScanAi

Length of output: 2532


Wrap cache eviction in try-except to prevent successful writes from failing.

The await FastAPICache.clear() call at lines 132-133 is not wrapped in its own error handling. If cache eviction fails, the exception propagates to the outer catch-all handler and converts the successful database update into a 500 response. Cache invalidation should be best-effort (with optional logging) so that write success is independent from cache backend failures.

Proposed fix
-            await FastAPICache.clear(namespace="markets")
+            try:
+                await FastAPICache.clear(namespace="markets")
+            except Exception:
+                # Keep DB write success independent from cache backend failures.
+                pass
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await FastAPICache.clear(namespace="markets")
try:
await FastAPICache.clear(namespace="markets")
except Exception:
# Keep DB write success independent from cache backend failures.
pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/vendors.py` around lines 132 - 133, The FastAPICache.clear() call
with namespace "markets" is not wrapped in error handling, which allows cache
backend failures to propagate and fail the entire write operation. Wrap the
await FastAPICache.clear(namespace="markets") call in its own try-except block
to catch any exceptions, optionally logging them for debugging purposes, but
allowing the function to continue and return success since the database write
itself was successful and cache invalidation should be treated as a best-effort
operation independent from the primary database operation.

return {
"success": True,
"vendor_id": vendor_id,
Expand Down
Loading