Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- create_test_users.py +99 -0
- main.py +171 -13
create_test_users.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Script to create test users for Plus and Pro tiers.
|
| 3 |
+
Run with: python3 create_test_users.py
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import hashlib
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
# Add parent directory to path to import MongoDBService
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
+
|
| 16 |
+
from services.mongodb_service import MongoDBService
|
| 17 |
+
|
| 18 |
+
def hash_password(password: str) -> str:
|
| 19 |
+
"""Hash password using SHA256 (matches backend login logic)"""
|
| 20 |
+
return hashlib.sha256(password.encode()).hexdigest()
|
| 21 |
+
|
| 22 |
+
def create_test_users():
|
| 23 |
+
"""Create Plus and Pro test users"""
|
| 24 |
+
try:
|
| 25 |
+
mongodb = MongoDBService()
|
| 26 |
+
|
| 27 |
+
# Test user credentials
|
| 28 |
+
test_users = [
|
| 29 |
+
{
|
| 30 |
+
"email": "[email protected]",
|
| 31 |
+
"plain_password": "PlusTester!123",
|
| 32 |
+
"password": hash_password("PlusTester!123"),
|
| 33 |
+
"name": "Plus Test User",
|
| 34 |
+
"subscription_tier": "Pro", # Maps to Plus tier
|
| 35 |
+
"domain_preferences": ["Technology", "Science"],
|
| 36 |
+
"phone_number": "+1234567890",
|
| 37 |
+
"age": 25,
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"email": "[email protected]",
|
| 41 |
+
"plain_password": "ProTester!123",
|
| 42 |
+
"password": hash_password("ProTester!123"),
|
| 43 |
+
"name": "Pro Test User",
|
| 44 |
+
"subscription_tier": "Enterprise", # Maps to Pro tier
|
| 45 |
+
"domain_preferences": ["Technology", "Science", "Politics", "Health"],
|
| 46 |
+
"phone_number": "+1234567891",
|
| 47 |
+
"age": 30,
|
| 48 |
+
}
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
print("π Creating test users...")
|
| 52 |
+
|
| 53 |
+
for user_data in test_users:
|
| 54 |
+
email = user_data["email"]
|
| 55 |
+
|
| 56 |
+
# Check if user already exists
|
| 57 |
+
existing = mongodb.users.find_one({"email": email})
|
| 58 |
+
if existing:
|
| 59 |
+
print(f"β οΈ User {email} already exists. Updating subscription tier...")
|
| 60 |
+
mongodb.update_user_subscription_tier(
|
| 61 |
+
str(existing["_id"]),
|
| 62 |
+
user_data["subscription_tier"]
|
| 63 |
+
)
|
| 64 |
+
print(f"β
Updated {email} to {user_data['subscription_tier']} tier")
|
| 65 |
+
else:
|
| 66 |
+
# Create new user - remove plain_password before inserting
|
| 67 |
+
user_insert_data = {k: v for k, v in user_data.items() if k != "plain_password"}
|
| 68 |
+
user_insert_data["created_at"] = datetime.utcnow()
|
| 69 |
+
user_insert_data["updated_at"] = datetime.utcnow()
|
| 70 |
+
|
| 71 |
+
result = mongodb.users.insert_one(user_insert_data)
|
| 72 |
+
user_data["_id"] = str(result.inserted_id)
|
| 73 |
+
user_data["id"] = str(result.inserted_id)
|
| 74 |
+
|
| 75 |
+
print(f"β
Created {email} with tier: {user_data['subscription_tier']}")
|
| 76 |
+
|
| 77 |
+
print("\nβ
Test users created/updated successfully!")
|
| 78 |
+
print("\nπ Login credentials:")
|
| 79 |
+
print("=" * 60)
|
| 80 |
+
for user_data in test_users:
|
| 81 |
+
print(f"\nEmail: {user_data['email']}")
|
| 82 |
+
print(f"Password: {user_data['plain_password']}")
|
| 83 |
+
if user_data['subscription_tier'] == "Pro":
|
| 84 |
+
print("Tier: Plus (subscription_tier: Pro)")
|
| 85 |
+
elif user_data['subscription_tier'] == "Enterprise":
|
| 86 |
+
print("Tier: Pro (subscription_tier: Enterprise)")
|
| 87 |
+
print("=" * 60)
|
| 88 |
+
|
| 89 |
+
mongodb.close()
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print(f"β Error creating test users: {e}")
|
| 93 |
+
import traceback
|
| 94 |
+
traceback.print_exc()
|
| 95 |
+
sys.exit(1)
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
create_test_users()
|
| 99 |
+
|
main.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Form, WebSocket, WebSocketDisconnect, Request
|
| 2 |
from typing import Optional, List, Dict, Any
|
| 3 |
-
from fastapi.responses import FileResponse
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from fastapi.staticfiles import StaticFiles
|
| 6 |
import uvicorn
|
|
@@ -64,6 +64,64 @@ app.mount("/static", StaticFiles(directory="public"), name="static")
|
|
| 64 |
app.mount("/frames", StaticFiles(directory="public/frames"), name="frames")
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
# Initialize verifiers and input processor
|
| 68 |
image_verifier = ImageVerifier()
|
| 69 |
video_verifier = VideoVerifier()
|
|
@@ -770,8 +828,11 @@ async def _verify_youtube_video(url: str, claim_context: str, claim_date: str) -
|
|
| 770 |
|
| 771 |
@app.post("/chatbot/verify")
|
| 772 |
async def chatbot_verify(
|
|
|
|
| 773 |
text_input: Optional[str] = Form(None),
|
| 774 |
-
files: Optional[List[UploadFile]] = File(None)
|
|
|
|
|
|
|
| 775 |
):
|
| 776 |
"""
|
| 777 |
Chatbot-friendly endpoint that intelligently processes input and routes to appropriate verification
|
|
@@ -781,6 +842,60 @@ async def chatbot_verify(
|
|
| 781 |
print(f"π DEBUG: text_input = {text_input}")
|
| 782 |
print(f"π DEBUG: files = {files}")
|
| 783 |
print(f"π DEBUG: files type = {type(files)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
received_files_meta: List[Dict[str, Any]] = []
|
| 785 |
if files:
|
| 786 |
for i, file in enumerate(files):
|
|
@@ -1298,28 +1413,61 @@ async def speech_to_text(
|
|
| 1298 |
raise HTTPException(status_code=500, detail=str(e))
|
| 1299 |
|
| 1300 |
|
| 1301 |
-
# Educational Content API Endpoints
|
| 1302 |
@app.get("/educational/modules")
|
| 1303 |
async def get_educational_modules():
|
| 1304 |
-
"""Get list of available educational modules"""
|
| 1305 |
try:
|
| 1306 |
-
|
| 1307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1308 |
except Exception as e:
|
|
|
|
| 1309 |
raise HTTPException(status_code=500, detail=str(e))
|
| 1310 |
|
| 1311 |
@app.get("/educational/modules/{module_id}")
|
| 1312 |
async def get_module_content(
|
| 1313 |
module_id: str,
|
| 1314 |
-
difficulty_level: str = "beginner"
|
| 1315 |
):
|
| 1316 |
-
"""Get educational content for a specific module"""
|
| 1317 |
try:
|
| 1318 |
-
|
| 1319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1320 |
)
|
| 1321 |
-
|
|
|
|
| 1322 |
except Exception as e:
|
|
|
|
| 1323 |
raise HTTPException(status_code=500, detail=str(e))
|
| 1324 |
|
| 1325 |
@app.post("/educational/contextual-learning")
|
|
@@ -1335,14 +1483,24 @@ async def get_contextual_learning(verification_result: Dict[str, Any]):
|
|
| 1335 |
|
| 1336 |
@app.post("/educational/clear-cache")
|
| 1337 |
async def clear_educational_cache():
|
| 1338 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1339 |
try:
|
| 1340 |
if educational_generator.redis_client:
|
| 1341 |
# Get all educational cache keys
|
| 1342 |
keys = educational_generator.redis_client.keys("educational:*")
|
| 1343 |
if keys:
|
| 1344 |
educational_generator.redis_client.delete(*keys)
|
| 1345 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1346 |
else:
|
| 1347 |
return {"message": "No cache entries found"}
|
| 1348 |
else:
|
|
|
|
| 1 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Form, WebSocket, WebSocketDisconnect, Request
|
| 2 |
from typing import Optional, List, Dict, Any
|
| 3 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from fastapi.staticfiles import StaticFiles
|
| 6 |
import uvicorn
|
|
|
|
| 64 |
app.mount("/frames", StaticFiles(directory="public/frames"), name="frames")
|
| 65 |
|
| 66 |
|
| 67 |
+
# ---------- Tier configuration ----------
|
| 68 |
+
|
| 69 |
+
# Public-facing tiers used across the product
|
| 70 |
+
NORMALIZED_TIERS = ("Free", "Plus", "Pro")
|
| 71 |
+
|
| 72 |
+
# Map stored subscription_tier / plan_name values to normalized tiers.
|
| 73 |
+
# This keeps backward compatibility with any existing users whose tier
|
| 74 |
+
# might still be stored as \"Pro\" or \"Enterprise\".
|
| 75 |
+
SUBSCRIPTION_TIER_MAPPING = {
|
| 76 |
+
"free": "Free",
|
| 77 |
+
"plus": "Plus",
|
| 78 |
+
"pro": "Plus", # legacy Pro maps to Plus
|
| 79 |
+
"enterprise": "Pro", # highest tier maps to Pro
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# Central limits per tier so they can be tuned in one place.
|
| 83 |
+
# These values are intentionally conservative to protect API costs.
|
| 84 |
+
TIER_LIMITS = {
|
| 85 |
+
"Free": {
|
| 86 |
+
"daily_verifications": 5,
|
| 87 |
+
"monthly_verifications": 25,
|
| 88 |
+
"max_chat_sessions": 1,
|
| 89 |
+
"max_messages_per_session": 10,
|
| 90 |
+
},
|
| 91 |
+
"Plus": {
|
| 92 |
+
"daily_verifications": 10,
|
| 93 |
+
"monthly_verifications": 50,
|
| 94 |
+
"max_chat_sessions": 5,
|
| 95 |
+
"max_messages_per_session": 50,
|
| 96 |
+
},
|
| 97 |
+
"Pro": {
|
| 98 |
+
"daily_verifications": 25,
|
| 99 |
+
"monthly_verifications": 200,
|
| 100 |
+
"max_chat_sessions": 20,
|
| 101 |
+
"max_messages_per_session": 200,
|
| 102 |
+
},
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def get_normalized_tier(raw_tier: str | None) -> str:
|
| 107 |
+
"""
|
| 108 |
+
Normalize any stored subscription_tier / plan_name to one of
|
| 109 |
+
the public-facing tiers: Free, Plus, Pro.
|
| 110 |
+
"""
|
| 111 |
+
if not raw_tier:
|
| 112 |
+
return "Free"
|
| 113 |
+
key = str(raw_tier).strip().lower()
|
| 114 |
+
return SUBSCRIPTION_TIER_MAPPING.get(key, "Free")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def get_tier_limits(raw_tier: str | None) -> dict:
|
| 118 |
+
"""
|
| 119 |
+
Return the limits dict for a given stored tier value.
|
| 120 |
+
"""
|
| 121 |
+
normalized = get_normalized_tier(raw_tier)
|
| 122 |
+
return TIER_LIMITS.get(normalized, TIER_LIMITS["Free"])
|
| 123 |
+
|
| 124 |
+
|
| 125 |
# Initialize verifiers and input processor
|
| 126 |
image_verifier = ImageVerifier()
|
| 127 |
video_verifier = VideoVerifier()
|
|
|
|
| 828 |
|
| 829 |
@app.post("/chatbot/verify")
|
| 830 |
async def chatbot_verify(
|
| 831 |
+
request: Request,
|
| 832 |
text_input: Optional[str] = Form(None),
|
| 833 |
+
files: Optional[List[UploadFile]] = File(None),
|
| 834 |
+
anonymous_id: Optional[str] = Form(None),
|
| 835 |
+
user_id: Optional[str] = Form(None),
|
| 836 |
):
|
| 837 |
"""
|
| 838 |
Chatbot-friendly endpoint that intelligently processes input and routes to appropriate verification
|
|
|
|
| 842 |
print(f"π DEBUG: text_input = {text_input}")
|
| 843 |
print(f"π DEBUG: files = {files}")
|
| 844 |
print(f"π DEBUG: files type = {type(files)}")
|
| 845 |
+
print(f"π DEBUG: anonymous_id = {anonymous_id}")
|
| 846 |
+
print(f"π DEBUG: user_id = {user_id}")
|
| 847 |
+
|
| 848 |
+
# Determine logical user key and tier for rate limiting
|
| 849 |
+
user_doc = None
|
| 850 |
+
raw_tier = "Free"
|
| 851 |
+
if user_id and mongodb_service:
|
| 852 |
+
try:
|
| 853 |
+
user_doc = mongodb_service.get_user_by_id(user_id)
|
| 854 |
+
except Exception as e:
|
| 855 |
+
logger.warning(
|
| 856 |
+
f"β οΈ Failed to load user {user_id} for tier resolution: {e}"
|
| 857 |
+
)
|
| 858 |
+
|
| 859 |
+
if user_doc:
|
| 860 |
+
raw_tier = user_doc.get("subscription_tier") or "Free"
|
| 861 |
+
else:
|
| 862 |
+
raw_tier = "Free"
|
| 863 |
+
|
| 864 |
+
limits = get_tier_limits(raw_tier)
|
| 865 |
+
key_host = getattr(request.client, "host", "unknown")
|
| 866 |
+
key = user_id or anonymous_id or f"ip:{key_host}"
|
| 867 |
+
|
| 868 |
+
if mongodb_service:
|
| 869 |
+
usage_info = mongodb_service.increment_usage_and_check_limits(
|
| 870 |
+
key=key,
|
| 871 |
+
feature="verification",
|
| 872 |
+
daily_limit=limits.get("daily_verifications"),
|
| 873 |
+
monthly_limit=limits.get("monthly_verifications"),
|
| 874 |
+
)
|
| 875 |
+
else:
|
| 876 |
+
usage_info = {
|
| 877 |
+
"allowed": True,
|
| 878 |
+
"tier_limits": {
|
| 879 |
+
"daily": limits.get("daily_verifications"),
|
| 880 |
+
"monthly": limits.get("monthly_verifications"),
|
| 881 |
+
},
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
if not usage_info.get("allowed", True):
|
| 885 |
+
normalized_tier = get_normalized_tier(raw_tier)
|
| 886 |
+
return JSONResponse(
|
| 887 |
+
status_code=429,
|
| 888 |
+
content={
|
| 889 |
+
"error": "verification_limit_reached",
|
| 890 |
+
"tier": normalized_tier,
|
| 891 |
+
"key": key,
|
| 892 |
+
"limits": usage_info.get("tier_limits"),
|
| 893 |
+
"usage": {
|
| 894 |
+
"daily": usage_info.get("daily"),
|
| 895 |
+
"monthly": usage_info.get("monthly"),
|
| 896 |
+
},
|
| 897 |
+
},
|
| 898 |
+
)
|
| 899 |
received_files_meta: List[Dict[str, Any]] = []
|
| 900 |
if files:
|
| 901 |
for i, file in enumerate(files):
|
|
|
|
| 1413 |
raise HTTPException(status_code=500, detail=str(e))
|
| 1414 |
|
| 1415 |
|
| 1416 |
+
# Educational Content API Endpoints - Now fetching from MongoDB weekly_posts
|
| 1417 |
@app.get("/educational/modules")
|
| 1418 |
async def get_educational_modules():
|
| 1419 |
+
"""Get list of available educational modules from MongoDB weekly_posts"""
|
| 1420 |
try:
|
| 1421 |
+
if not mongodb_service:
|
| 1422 |
+
raise HTTPException(status_code=503, detail="MongoDB service not available")
|
| 1423 |
+
|
| 1424 |
+
modules_list = mongodb_service.get_educational_modules_list()
|
| 1425 |
+
response_data = {
|
| 1426 |
+
"modules": modules_list,
|
| 1427 |
+
"total": len(modules_list)
|
| 1428 |
+
}
|
| 1429 |
+
# Return with no-cache headers to prevent stale cache in production
|
| 1430 |
+
return JSONResponse(
|
| 1431 |
+
content=response_data,
|
| 1432 |
+
headers={
|
| 1433 |
+
"Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
|
| 1434 |
+
"Pragma": "no-cache",
|
| 1435 |
+
"Expires": "0"
|
| 1436 |
+
}
|
| 1437 |
+
)
|
| 1438 |
+
except HTTPException:
|
| 1439 |
+
raise
|
| 1440 |
except Exception as e:
|
| 1441 |
+
logger.error(f"Failed to get educational modules: {e}")
|
| 1442 |
raise HTTPException(status_code=500, detail=str(e))
|
| 1443 |
|
| 1444 |
@app.get("/educational/modules/{module_id}")
|
| 1445 |
async def get_module_content(
|
| 1446 |
module_id: str,
|
| 1447 |
+
difficulty_level: str = "beginner" # Kept for backward compatibility but not used
|
| 1448 |
):
|
| 1449 |
+
"""Get educational content for a specific module from MongoDB weekly_posts"""
|
| 1450 |
try:
|
| 1451 |
+
if not mongodb_service:
|
| 1452 |
+
raise HTTPException(status_code=503, detail="MongoDB service not available")
|
| 1453 |
+
|
| 1454 |
+
content = mongodb_service.get_educational_module_by_id(module_id)
|
| 1455 |
+
if not content:
|
| 1456 |
+
raise HTTPException(status_code=404, detail=f"Module '{module_id}' not found")
|
| 1457 |
+
|
| 1458 |
+
# Return with no-cache headers to prevent stale cache in production
|
| 1459 |
+
return JSONResponse(
|
| 1460 |
+
content=content,
|
| 1461 |
+
headers={
|
| 1462 |
+
"Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
|
| 1463 |
+
"Pragma": "no-cache",
|
| 1464 |
+
"Expires": "0"
|
| 1465 |
+
}
|
| 1466 |
)
|
| 1467 |
+
except HTTPException:
|
| 1468 |
+
raise
|
| 1469 |
except Exception as e:
|
| 1470 |
+
logger.error(f"Failed to get module content: {e}")
|
| 1471 |
raise HTTPException(status_code=500, detail=str(e))
|
| 1472 |
|
| 1473 |
@app.post("/educational/contextual-learning")
|
|
|
|
| 1483 |
|
| 1484 |
@app.post("/educational/clear-cache")
|
| 1485 |
async def clear_educational_cache():
|
| 1486 |
+
"""
|
| 1487 |
+
Clear all educational content from Redis cache.
|
| 1488 |
+
|
| 1489 |
+
Note: The /educational/modules endpoints now use no-cache headers
|
| 1490 |
+
to prevent browser/CDN caching. This endpoint is mainly for clearing
|
| 1491 |
+
any legacy Redis cache entries.
|
| 1492 |
+
"""
|
| 1493 |
try:
|
| 1494 |
if educational_generator.redis_client:
|
| 1495 |
# Get all educational cache keys
|
| 1496 |
keys = educational_generator.redis_client.keys("educational:*")
|
| 1497 |
if keys:
|
| 1498 |
educational_generator.redis_client.delete(*keys)
|
| 1499 |
+
return {
|
| 1500 |
+
"message": f"Cleared {len(keys)} cache entries",
|
| 1501 |
+
"keys": keys,
|
| 1502 |
+
"note": "Educational endpoints use no-cache headers to prevent stale data"
|
| 1503 |
+
}
|
| 1504 |
else:
|
| 1505 |
return {"message": "No cache entries found"}
|
| 1506 |
else:
|