SHAFI commited on
Commit ·
5e81c7b
1
Parent(s): 586568d
newsletter bug erro '500 NOT FOUND' Fixed
Browse files- app/routes/admin.py +20 -12
- app/routes/subscription.py +10 -10
- app/services/newsletter_service.py +3 -7
- app/services/scheduler.py +2 -1
- tools/test_audio_api.py +68 -0
- tools/test_audio_quick.py +73 -0
- tools/test_cloud_schema.py +77 -0
- tools/test_validation_fix.py +49 -0
app/routes/admin.py
CHANGED
|
@@ -445,7 +445,7 @@ async def send_newsletter_now(preference: str = "Weekly"):
|
|
| 445 |
@router.get("/subscribers/analytics")
|
| 446 |
async def get_subscriber_analytics():
|
| 447 |
"""
|
| 448 |
-
Get subscriber distribution by preference
|
| 449 |
|
| 450 |
Shows how many subscribers have chosen each newsletter timing.
|
| 451 |
Useful for understanding user preferences and planning content strategy.
|
|
@@ -454,17 +454,17 @@ async def get_subscriber_analytics():
|
|
| 454 |
Total active subscribers and breakdown by preference
|
| 455 |
"""
|
| 456 |
try:
|
| 457 |
-
from app.services.
|
| 458 |
|
| 459 |
-
|
| 460 |
|
| 461 |
-
if not
|
| 462 |
raise HTTPException(
|
| 463 |
status_code=503,
|
| 464 |
-
detail="
|
| 465 |
)
|
| 466 |
|
| 467 |
-
all_subscribers =
|
| 468 |
|
| 469 |
# Calculate preference distribution
|
| 470 |
preference_counts = {
|
|
@@ -479,18 +479,26 @@ async def get_subscriber_analytics():
|
|
| 479 |
total_count = len(all_subscribers)
|
| 480 |
|
| 481 |
for sub in all_subscribers:
|
| 482 |
-
if sub.get('
|
| 483 |
active_count += 1
|
| 484 |
-
|
| 485 |
-
if
|
| 486 |
-
preference_counts[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
return {
|
| 489 |
"total_subscribers": total_count,
|
| 490 |
"active_subscribers": active_count,
|
| 491 |
-
"
|
| 492 |
"distribution_by_preference": preference_counts,
|
| 493 |
-
"
|
| 494 |
pref: round((count / active_count * 100), 2) if active_count > 0 else 0
|
| 495 |
for pref, count in preference_counts.items()
|
| 496 |
}
|
|
|
|
| 445 |
@router.get("/subscribers/analytics")
|
| 446 |
async def get_subscriber_analytics():
|
| 447 |
"""
|
| 448 |
+
Get subscriber distribution by preference from Appwrite
|
| 449 |
|
| 450 |
Shows how many subscribers have chosen each newsletter timing.
|
| 451 |
Useful for understanding user preferences and planning content strategy.
|
|
|
|
| 454 |
Total active subscribers and breakdown by preference
|
| 455 |
"""
|
| 456 |
try:
|
| 457 |
+
from app.services.appwrite_db import get_appwrite_db
|
| 458 |
|
| 459 |
+
appwrite_db = get_appwrite_db()
|
| 460 |
|
| 461 |
+
if not appwrite_db.initialized:
|
| 462 |
raise HTTPException(
|
| 463 |
status_code=503,
|
| 464 |
+
detail="Appwrite database not available"
|
| 465 |
)
|
| 466 |
|
| 467 |
+
all_subscribers = await appwrite_db.get_all_subscribers()
|
| 468 |
|
| 469 |
# Calculate preference distribution
|
| 470 |
preference_counts = {
|
|
|
|
| 479 |
total_count = len(all_subscribers)
|
| 480 |
|
| 481 |
for sub in all_subscribers:
|
| 482 |
+
if sub.get('isActive', True):
|
| 483 |
active_count += 1
|
| 484 |
+
# Count each preference subscription
|
| 485 |
+
if sub.get('sub_morning', False):
|
| 486 |
+
preference_counts['Morning'] += 1
|
| 487 |
+
if sub.get('sub_afternoon', False):
|
| 488 |
+
preference_counts['Afternoon'] += 1
|
| 489 |
+
if sub.get('sub_evening', False):
|
| 490 |
+
preference_counts['Evening'] += 1
|
| 491 |
+
if sub.get('sub_weekly', False):
|
| 492 |
+
preference_counts['Weekly'] += 1
|
| 493 |
+
if sub.get('sub_monthly', False):
|
| 494 |
+
preference_counts['Monthly'] += 1
|
| 495 |
|
| 496 |
return {
|
| 497 |
"total_subscribers": total_count,
|
| 498 |
"active_subscribers": active_count,
|
| 499 |
+
"inactive": total_count - active_count,
|
| 500 |
"distribution_by_preference": preference_counts,
|
| 501 |
+
"percentage_distribution": {
|
| 502 |
pref: round((count / active_count * 100), 2) if active_count > 0 else 0
|
| 503 |
for pref, count in preference_counts.items()
|
| 504 |
}
|
app/routes/subscription.py
CHANGED
|
@@ -238,17 +238,17 @@ async def unsubscribe_post(request: UnsubscribeRequest):
|
|
| 238 |
|
| 239 |
@router.get("/subscribers/count")
|
| 240 |
async def get_subscriber_count():
|
| 241 |
-
"""Get total number of active subscribers"""
|
| 242 |
try:
|
| 243 |
-
|
| 244 |
-
subscribers =
|
| 245 |
|
| 246 |
-
active_count = sum(1 for s in subscribers if s.get('
|
| 247 |
|
| 248 |
return {
|
| 249 |
"total": len(subscribers),
|
| 250 |
"active": active_count,
|
| 251 |
-
"
|
| 252 |
}
|
| 253 |
|
| 254 |
except Exception as e:
|
|
@@ -265,19 +265,19 @@ async def send_newsletter(
|
|
| 265 |
category: str = "ai"
|
| 266 |
):
|
| 267 |
"""
|
| 268 |
-
Send newsletter to all subscribers
|
| 269 |
|
| 270 |
- Fetches latest news from specified category
|
| 271 |
- Sends to all active subscribers
|
| 272 |
- Returns send statistics
|
| 273 |
"""
|
| 274 |
try:
|
| 275 |
-
|
| 276 |
brevo = get_brevo_service()
|
| 277 |
|
| 278 |
-
# Get all active subscribers
|
| 279 |
-
subscribers =
|
| 280 |
-
active_subscribers = [s for s in subscribers if s.get('
|
| 281 |
|
| 282 |
if not active_subscribers:
|
| 283 |
return {
|
|
|
|
| 238 |
|
| 239 |
@router.get("/subscribers/count")
|
| 240 |
async def get_subscriber_count():
|
| 241 |
+
"""Get total number of active subscribers from Appwrite"""
|
| 242 |
try:
|
| 243 |
+
appwrite_db = get_appwrite_db()
|
| 244 |
+
subscribers = await appwrite_db.get_all_subscribers()
|
| 245 |
|
| 246 |
+
active_count = sum(1 for s in subscribers if s.get('isActive', True))
|
| 247 |
|
| 248 |
return {
|
| 249 |
"total": len(subscribers),
|
| 250 |
"active": active_count,
|
| 251 |
+
"inactive": len(subscribers) - active_count
|
| 252 |
}
|
| 253 |
|
| 254 |
except Exception as e:
|
|
|
|
| 265 |
category: str = "ai"
|
| 266 |
):
|
| 267 |
"""
|
| 268 |
+
Send newsletter to all subscribers (LEGACY ENDPOINT - Use scheduled newsletters instead)
|
| 269 |
|
| 270 |
- Fetches latest news from specified category
|
| 271 |
- Sends to all active subscribers
|
| 272 |
- Returns send statistics
|
| 273 |
"""
|
| 274 |
try:
|
| 275 |
+
appwrite_db = get_appwrite_db()
|
| 276 |
brevo = get_brevo_service()
|
| 277 |
|
| 278 |
+
# Get all active subscribers from Appwrite
|
| 279 |
+
subscribers = await appwrite_db.get_all_subscribers()
|
| 280 |
+
active_subscribers = [s for s in subscribers if s.get('isActive', True)]
|
| 281 |
|
| 282 |
if not active_subscribers:
|
| 283 |
return {
|
app/services/newsletter_service.py
CHANGED
|
@@ -281,13 +281,9 @@ async def send_scheduled_newsletter(preference: str) -> Dict[str, int]:
|
|
| 281 |
return result
|
| 282 |
|
| 283 |
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
Used by admin endpoints and testing.
|
| 288 |
-
"""
|
| 289 |
-
firebase = get_firebase_service()
|
| 290 |
-
return firebase.get_subscribers_by_preference(preference)
|
| 291 |
|
| 292 |
|
| 293 |
async def preview_newsletter_content(preference: str) -> Dict:
|
|
|
|
| 281 |
return result
|
| 282 |
|
| 283 |
|
| 284 |
+
|
| 285 |
+
# Note: Subscriber queries now use Appwrite directly via appwrite_db.get_subscribers_by_preference()
|
| 286 |
+
# See newsletter_service.py line 211
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
|
| 289 |
async def preview_newsletter_content(preference: str) -> Dict:
|
app/services/scheduler.py
CHANGED
|
@@ -560,7 +560,8 @@ async def trigger_newsletter_now(preference: str):
|
|
| 560 |
"""Manually trigger newsletter"""
|
| 561 |
from app.services.newsletter_service import send_scheduled_newsletter
|
| 562 |
logger.info(f"🔧 [MANUAL TRIGGER] Running {preference} newsletter job NOW...")
|
| 563 |
-
await send_scheduled_newsletter(preference)
|
|
|
|
| 564 |
|
| 565 |
|
| 566 |
|
|
|
|
| 560 |
"""Manually trigger newsletter"""
|
| 561 |
from app.services.newsletter_service import send_scheduled_newsletter
|
| 562 |
logger.info(f"🔧 [MANUAL TRIGGER] Running {preference} newsletter job NOW...")
|
| 563 |
+
result = await send_scheduled_newsletter(preference)
|
| 564 |
+
return result
|
| 565 |
|
| 566 |
|
| 567 |
|
tools/test_audio_api.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script for audio generation API
|
| 3 |
+
Tests the complete flow from request to response
|
| 4 |
+
"""
|
| 5 |
+
import requests
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
# Test configuration
|
| 9 |
+
API_URL = "http://127.0.0.1:8000/api/audio/generate"
|
| 10 |
+
|
| 11 |
+
# Sample article data
|
| 12 |
+
test_payload = {
|
| 13 |
+
"article_url": "https://www.rawstory.com%2FTrump-ic e-2675183222%2F&title=Analyst%20issues%20dire%20warning%20about%20Trump%27s%20%27horrifying%27%20directive%20to%20ICE%20to%20detain%20immigration%20judges",
|
| 14 |
+
"title": "Test Article for Audio Generation",
|
| 15 |
+
"image_url": "https://example.com/image.jpg",
|
| 16 |
+
"category": "ai"
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
print("=" * 60)
|
| 20 |
+
print("🧪 Testing Audio Generation API")
|
| 21 |
+
print("=" * 60)
|
| 22 |
+
print(f"\n📍 Endpoint: {API_URL}")
|
| 23 |
+
print(f"\n📦 Payload:")
|
| 24 |
+
print(json.dumps(test_payload, indent=2))
|
| 25 |
+
print("\n" + "=" * 60)
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
print("\n🚀 Sending POST request...")
|
| 29 |
+
response = requests.post(
|
| 30 |
+
API_URL,
|
| 31 |
+
json=test_payload,
|
| 32 |
+
headers={"Content-Type": "application/json"},
|
| 33 |
+
timeout=60 # Audio generation can take time
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
print(f"\n📊 Response Status: {response.status_code}")
|
| 37 |
+
print(f"\n📄 Response Body:")
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
response_data = response.json()
|
| 41 |
+
print(json.dumps(response_data, indent=2))
|
| 42 |
+
|
| 43 |
+
if response.status_code == 200:
|
| 44 |
+
if response_data.get('success'):
|
| 45 |
+
print("\n✅ SUCCESS! Audio generated successfully")
|
| 46 |
+
print(f"🔊 Audio URL: {response_data.get('audio_url')}")
|
| 47 |
+
else:
|
| 48 |
+
print("\n❌ FAILED: success=False in response")
|
| 49 |
+
print(f"💬 Message: {response_data.get('message', 'No message')}")
|
| 50 |
+
else:
|
| 51 |
+
print(f"\n❌ HTTP ERROR: {response.status_code}")
|
| 52 |
+
print(f"💬 Detail: {response_data.get('detail', 'No detail')}")
|
| 53 |
+
|
| 54 |
+
except json.JSONDecodeError:
|
| 55 |
+
print("⚠️ Response is not valid JSON:")
|
| 56 |
+
print(response.text)
|
| 57 |
+
|
| 58 |
+
except requests.exceptions.ConnectionError:
|
| 59 |
+
print("\n❌ CONNECTION ERROR: Cannot connect to backend")
|
| 60 |
+
print("Make sure the backend is running on http://127.0.0.1:8000")
|
| 61 |
+
|
| 62 |
+
except requests.exceptions.Timeout:
|
| 63 |
+
print("\n❌ TIMEOUT: Request took longer than 60 seconds")
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"\n❌ UNEXPECTED ERROR: {type(e).__name__}: {e}")
|
| 67 |
+
|
| 68 |
+
print("\n" + "=" * 60)
|
tools/test_audio_quick.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quick test script for audio API after fixes
|
| 3 |
+
"""
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
print("=" * 60)
|
| 8 |
+
print("🎵 AUDIO API QUICK TEST")
|
| 9 |
+
print("=" * 60)
|
| 10 |
+
|
| 11 |
+
# Test 1: Health Check
|
| 12 |
+
print("\n1️⃣ Testing backend health...")
|
| 13 |
+
try:
|
| 14 |
+
health = requests.get("http://127.0.0.1:8000/health", timeout=5)
|
| 15 |
+
print(f" ✅ Backend is running (Status: {health.status_code})")
|
| 16 |
+
except Exception as e:
|
| 17 |
+
print(f" ❌ Backend not responding: {e}")
|
| 18 |
+
exit(1)
|
| 19 |
+
|
| 20 |
+
# Test 2: Audio Endpoint
|
| 21 |
+
print("\n2️⃣ Testing /api/audio/generate endpoint...")
|
| 22 |
+
payload = {
|
| 23 |
+
"article_url": "https://www.example.com/test-article",
|
| 24 |
+
"title": "Test Article Title",
|
| 25 |
+
"category": "ai",
|
| 26 |
+
"image_url": "https://www.example.com/image.jpg"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
print(f"\n📦 Payload:")
|
| 30 |
+
print(json.dumps(payload, indent=2))
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
print("\n⏳ Sending request (this may take 10-30 seconds)...")
|
| 34 |
+
response = requests.post(
|
| 35 |
+
"http://127.0.0.1:8000/api/audio/generate",
|
| 36 |
+
json=payload,
|
| 37 |
+
timeout=60
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
print(f"\n📊 Status Code: {response.status_code}")
|
| 41 |
+
|
| 42 |
+
if response.status_code == 200:
|
| 43 |
+
data = response.json()
|
| 44 |
+
if data.get('success'):
|
| 45 |
+
print("✅ SUCCESS! Audio generated")
|
| 46 |
+
print(f"🔊 Audio URL: {data.get('audio_url')}")
|
| 47 |
+
else:
|
| 48 |
+
print("⚠️ Request succeeded but success=False")
|
| 49 |
+
print(f"Message: {data.get('message', 'No message')}")
|
| 50 |
+
else:
|
| 51 |
+
print(f"❌ HTTP {response.status_code}")
|
| 52 |
+
try:
|
| 53 |
+
error_data = response.json()
|
| 54 |
+
print(f"Detail: {error_data.get('detail', 'No detail')}")
|
| 55 |
+
except:
|
| 56 |
+
print(f"Response: {response.text[:200]}")
|
| 57 |
+
|
| 58 |
+
except requests.exceptions.Timeout:
|
| 59 |
+
print("❌ Request timed out (>60s)")
|
| 60 |
+
print("Possible causes:")
|
| 61 |
+
print(" - GROQ_API_KEY not set in .env")
|
| 62 |
+
print(" - Appwrite bucket 'audio-summaries' doesn't exist")
|
| 63 |
+
print(" - Article content scraping failed")
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"❌ Error: {type(e).__name__}: {e}")
|
| 67 |
+
|
| 68 |
+
print("\n" + "=" * 60)
|
| 69 |
+
print("\n💡 If test failed, check:")
|
| 70 |
+
print(" 1. GROQ_API_KEY in .env file")
|
| 71 |
+
print(" 2. 'audio-summaries' bucket exists in Appwrite")
|
| 72 |
+
print(" 3. Backend terminal logs for detailed error")
|
| 73 |
+
print("=" * 60)
|
tools/test_cloud_schema.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
# Add parent directory to path
|
| 6 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 7 |
+
|
| 8 |
+
from app.models import Article
|
| 9 |
+
from app.services.appwrite_db import AppwriteDatabase
|
| 10 |
+
from app.config import settings
|
| 11 |
+
|
| 12 |
+
# MOCK SETTINGS FOR TEST
|
| 13 |
+
# We need to simulate that we are saving to the CLOUD collection
|
| 14 |
+
settings.APPWRITE_CLOUD_COLLECTION_ID = "mock_cloud_collection_id"
|
| 15 |
+
settings.APPWRITE_DATABASE_ID = "mock_db_id"
|
| 16 |
+
|
| 17 |
+
def test_cloud_schema_mapping():
|
| 18 |
+
print("🧪 Testing Cloud Collection Schema Mapping...")
|
| 19 |
+
|
| 20 |
+
# 1. Create a dummy AppwriteDatabase instance
|
| 21 |
+
# We will mock the tablesDB.create_row method to intercept the data
|
| 22 |
+
db = AppwriteDatabase()
|
| 23 |
+
|
| 24 |
+
# Mock the internal tablesDB.create_row to just print the data
|
| 25 |
+
class MockDB:
|
| 26 |
+
def create_row(self, database_id, collection_id, document_id, data):
|
| 27 |
+
print(f"\n📝 INTERCEPTED WRITE to {collection_id}:")
|
| 28 |
+
print(f" IDs: {document_id}")
|
| 29 |
+
print(f" Keys: {list(data.keys())}")
|
| 30 |
+
|
| 31 |
+
# CHECK 1: 'image' should exist, 'image_url' should NOT
|
| 32 |
+
if 'image' in data and 'image_url' not in data:
|
| 33 |
+
print(" ✅ PASS: 'image' used instead of 'image_url'")
|
| 34 |
+
else:
|
| 35 |
+
print(f" ❌ FAIL: Keys are wrong! {list(data.keys())}")
|
| 36 |
+
|
| 37 |
+
# CHECK 2: 'publishedAt' should exist, 'published_at' should NOT
|
| 38 |
+
if 'publishedAt' in data and 'published_at' not in data:
|
| 39 |
+
print(" ✅ PASS: 'publishedAt' used instead of 'published_at'")
|
| 40 |
+
else:
|
| 41 |
+
print(f" ❌ FAIL: Date keys are wrong! {list(data.keys())}")
|
| 42 |
+
|
| 43 |
+
return {'$id': document_id}
|
| 44 |
+
|
| 45 |
+
db.tablesDB = MockDB()
|
| 46 |
+
db.initialized = True # Force init for test
|
| 47 |
+
|
| 48 |
+
# 2. Create a test article (Cloud Category)
|
| 49 |
+
# This category 'cloud-aws' triggers the get_collection_id -> cloud collection logic
|
| 50 |
+
article = {
|
| 51 |
+
'title': 'Test Cloud Article',
|
| 52 |
+
'url': 'https://aws.amazon.com/test',
|
| 53 |
+
'image_url': 'https://aws.amazon.com/image.jpg', # New schema key
|
| 54 |
+
'published_at': datetime.now(), # New schema key
|
| 55 |
+
'source': 'AWS Blog',
|
| 56 |
+
'category': 'cloud-aws' # CRITICAL: This routes to Cloud Collection
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
print(f"\n📋 Input Article Keys: {list(article.keys())}")
|
| 60 |
+
|
| 61 |
+
# 3. Import asyncio and run save_articles
|
| 62 |
+
import asyncio
|
| 63 |
+
|
| 64 |
+
# Patch get_collection_id to return our mock ID
|
| 65 |
+
original_get_collection_id = db.get_collection_id
|
| 66 |
+
db.get_collection_id = lambda cat: settings.APPWRITE_CLOUD_COLLECTION_ID
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
asyncio.run(db.save_articles([article]))
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"Error: {e}")
|
| 72 |
+
finally:
|
| 73 |
+
# Restore (good practice, though script ends)
|
| 74 |
+
db.get_collection_id = original_get_collection_id
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
test_cloud_schema_mapping()
|
tools/test_validation_fix.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
# Add parent directory to path
|
| 6 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 7 |
+
|
| 8 |
+
from app.models import Article
|
| 9 |
+
from app.utils.data_validation import is_valid_article, sanitize_article
|
| 10 |
+
|
| 11 |
+
def test_validation():
|
| 12 |
+
print("🧪 Testing Data Validation Fix...")
|
| 13 |
+
|
| 14 |
+
# 1. Create a Pydantic Article (snake_case fields)
|
| 15 |
+
article = Article(
|
| 16 |
+
title="Test Article Title For Validation",
|
| 17 |
+
url="https://example.com/test-article",
|
| 18 |
+
published_at=datetime.now(),
|
| 19 |
+
image_url="https://example.com/image.jpg",
|
| 20 |
+
source="Test Source",
|
| 21 |
+
category="ai"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
print(f"\n📋 Model Data: {article.model_dump()}")
|
| 25 |
+
|
| 26 |
+
# 2. Test is_valid_article
|
| 27 |
+
is_valid = is_valid_article(article)
|
| 28 |
+
print(f"\n✅ is_valid_article: {is_valid}")
|
| 29 |
+
|
| 30 |
+
if is_valid:
|
| 31 |
+
print(" -> PASSED: Pydantic model validated successfully!")
|
| 32 |
+
else:
|
| 33 |
+
print(" -> FAILED: Pydantic model rejected!")
|
| 34 |
+
|
| 35 |
+
# 3. Test sanitize_article
|
| 36 |
+
sanitized = sanitize_article(article)
|
| 37 |
+
print(f"\n🧹 Sanitized Data Keys: {list(sanitized.keys())}")
|
| 38 |
+
print(f" publishedAt: {sanitized.get('publishedAt')}")
|
| 39 |
+
print(f" published_at: {sanitized.get('published_at')}")
|
| 40 |
+
print(f" image: {sanitized.get('image')}")
|
| 41 |
+
print(f" image_url: {sanitized.get('image_url')}")
|
| 42 |
+
|
| 43 |
+
if sanitized.get('publishedAt') and sanitized.get('image_url'):
|
| 44 |
+
print(" -> PASSED: Sanitization preserved critical fields!")
|
| 45 |
+
else:
|
| 46 |
+
print(" -> FAILED: Sanitization missing fields!")
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
test_validation()
|