Automated Action 90c1cdef34 Setup News Aggregation Service
- Fix code linting issues
- Update README with detailed documentation
- Configure database paths for the current environment
- Create necessary directory structure

The News Aggregation Service is now ready to use with FastAPI and SQLite.
2025-05-27 18:50:11 +00:00

285 lines
8.5 KiB
Python

from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from sqlalchemy.orm import Session
from app.api import deps
from app.models.user import User
from app.models.news import NewsArticle, NewsSource, NewsCategory, SavedArticle
from app.schemas.news import (
NewsSource as NewsSourceSchema,
NewsCategory as NewsCategorySchema,
SavedArticle as SavedArticleSchema,
SavedArticleCreate,
NewsResponse,
)
from app.services.news import (
get_news_articles,
fetch_and_store_news,
get_saved_articles_for_user,
save_article_for_user,
delete_saved_article,
)
from app.services.user import get_user_preference
router = APIRouter()
@router.get("/", response_model=NewsResponse)
async def get_news(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 25,
keywords: Optional[str] = None,
sources: Optional[str] = None,
categories: Optional[str] = None,
countries: Optional[str] = None,
languages: Optional[str] = None,
current_user: Optional[User] = Depends(deps.get_current_active_user),
refresh: bool = False,
background_tasks: BackgroundTasks = None,
):
"""
Retrieve news articles with optional filtering.
If refresh is True, the API will fetch fresh news from Mediastack.
Otherwise, it will return news articles from the database.
"""
# Parse filters
source_ids = None
if sources:
source_names = [s.strip() for s in sources.split(",")]
source_query = db.query(NewsSource).filter(NewsSource.name.in_(source_names))
source_ids = [source.id for source in source_query.all()]
category_ids = None
if categories:
category_names = [c.strip() for c in categories.split(",")]
category_query = db.query(NewsCategory).filter(NewsCategory.name.in_(category_names))
category_ids = [category.id for category in category_query.all()]
country_list = None
if countries:
country_list = [c.strip() for c in countries.split(",")]
language_list = None
if languages:
language_list = [lang.strip() for lang in languages.split(",")]
# If refresh is requested, fetch news from the API
if refresh and background_tasks:
background_tasks.add_task(
fetch_and_store_news,
db,
keywords=keywords,
sources=sources,
categories=categories,
countries=countries,
languages=languages,
)
# Get news from the database
articles, total = get_news_articles(
db,
skip=skip,
limit=limit,
keywords=keywords,
source_ids=source_ids,
category_ids=category_ids,
countries=country_list,
languages=language_list,
)
return {"items": articles, "total": total}
@router.get("/personalized", response_model=NewsResponse)
async def get_personalized_news(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 25,
current_user: User = Depends(deps.get_current_active_user),
refresh: bool = False,
background_tasks: BackgroundTasks = None,
):
"""
Retrieve news articles based on the user's preferences.
"""
# Get user preferences
preferences = get_user_preference(db, current_user.id)
if not preferences:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User preferences not found",
)
# If refresh is requested, fetch news from the API
if refresh and background_tasks:
background_tasks.add_task(
fetch_and_store_news,
db,
keywords=preferences.keywords,
sources=preferences.sources,
categories=preferences.categories,
countries=preferences.countries,
languages=preferences.languages,
)
# Parse filters from preferences
source_ids = None
if preferences.sources:
source_names = [s.strip() for s in preferences.sources.split(",")]
source_query = db.query(NewsSource).filter(NewsSource.name.in_(source_names))
source_ids = [source.id for source in source_query.all()]
category_ids = None
if preferences.categories:
category_names = [c.strip() for c in preferences.categories.split(",")]
category_query = db.query(NewsCategory).filter(NewsCategory.name.in_(category_names))
category_ids = [category.id for category in category_query.all()]
country_list = None
if preferences.countries:
country_list = [c.strip() for c in preferences.countries.split(",")]
language_list = None
if preferences.languages:
language_list = [lang.strip() for lang in preferences.languages.split(",")]
# Get news from the database
articles, total = get_news_articles(
db,
skip=skip,
limit=limit,
keywords=preferences.keywords,
source_ids=source_ids,
category_ids=category_ids,
countries=country_list,
languages=language_list,
)
return {"items": articles, "total": total}
@router.get("/refresh", response_model=NewsResponse)
async def refresh_news(
db: Session = Depends(deps.get_db),
keywords: Optional[str] = None,
sources: Optional[str] = None,
categories: Optional[str] = None,
countries: Optional[str] = None,
languages: Optional[str] = None,
limit: int = 100,
current_user: User = Depends(deps.get_current_active_user),
):
"""
Fetch fresh news from Mediastack API and return them.
This endpoint will fetch news from the API and store them in the database.
"""
articles = await fetch_and_store_news(
db,
keywords=keywords,
sources=sources,
categories=categories,
countries=countries,
languages=languages,
limit=limit,
)
return {"items": articles, "total": len(articles)}
@router.get("/sources", response_model=List[NewsSourceSchema])
async def get_sources(
db: Session = Depends(deps.get_db),
):
"""
Get a list of news sources.
"""
sources = db.query(NewsSource).all()
return sources
@router.get("/categories", response_model=List[NewsCategorySchema])
async def get_categories(
db: Session = Depends(deps.get_db),
):
"""
Get a list of news categories.
"""
categories = db.query(NewsCategory).all()
return categories
@router.get("/saved", response_model=List[SavedArticleSchema])
async def get_saved_articles(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(deps.get_current_active_user),
):
"""
Get articles saved by the current user.
"""
saved_articles, total = get_saved_articles_for_user(
db, user_id=current_user.id, skip=skip, limit=limit
)
return saved_articles
@router.post("/saved", response_model=SavedArticleSchema)
async def save_article(
*,
db: Session = Depends(deps.get_db),
article_in: SavedArticleCreate,
current_user: User = Depends(deps.get_current_active_user),
):
"""
Save an article for the current user.
"""
# Check if article exists
article = db.query(NewsArticle).filter(NewsArticle.id == article_in.article_id).first()
if not article:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Article not found",
)
# Check if already saved
existing = (
db.query(SavedArticle)
.filter(
SavedArticle.user_id == current_user.id,
SavedArticle.article_id == article_in.article_id,
)
.first()
)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Article already saved",
)
saved_article = save_article_for_user(
db, user_id=current_user.id, article_id=article_in.article_id, notes=article_in.notes
)
return saved_article
@router.delete("/saved/{saved_article_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
async def remove_saved_article(
saved_article_id: int,
db: Session = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_active_user),
):
"""
Remove a saved article for the current user.
"""
success = delete_saved_article(db, user_id=current_user.id, saved_article_id=saved_article_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Saved article not found",
)
return None