Aniworld/src/server/api/analytics.py
Lukas 6a6ae7e059 fix: resolve all failing tests (701 tests now passing)
- Add missing src/server/api/__init__.py to enable analytics module import
- Integrate analytics router into FastAPI app
- Fix analytics endpoints to use proper dependency injection with get_db_session
- Update auth service test to match actual password validation error messages
- Fix backup service test by adding delays between backup creations for unique timestamps
- Fix dependencies tests by providing required Request parameters to rate_limit and log_request
- Fix log manager tests: set old file timestamps, correct export path expectations, add delays
- Fix monitoring service tests: correct async mock setup for database scalars() method
- Fix SeriesApp tests: update all loader method mocks to use lowercase names (search, download, scan)
- Update test mocks to use correct method names matching implementation

All 701 tests now passing with 0 failures.
2025-10-23 21:00:34 +02:00

259 lines
7.6 KiB
Python

"""Analytics API endpoints for accessing system analytics and reports.
Provides REST API endpoints for querying analytics data including download
statistics, series popularity, storage analysis, and performance reports.
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from src.server.database.connection import get_db_session
from src.server.services.analytics_service import get_analytics_service
router = APIRouter(prefix="/api/analytics", tags=["analytics"])
class DownloadStatsResponse(BaseModel):
"""Download statistics response model."""
total_downloads: int
successful_downloads: int
failed_downloads: int
total_bytes_downloaded: int
average_speed_mbps: float
success_rate: float
average_duration_seconds: float
class SeriesPopularityResponse(BaseModel):
"""Series popularity response model."""
series_name: str
download_count: int
total_size_bytes: int
last_download: Optional[str]
success_rate: float
class StorageAnalysisResponse(BaseModel):
"""Storage analysis response model."""
total_storage_bytes: int
used_storage_bytes: int
free_storage_bytes: int
storage_percent_used: float
downloads_directory_size_bytes: int
cache_directory_size_bytes: int
logs_directory_size_bytes: int
class PerformanceReportResponse(BaseModel):
"""Performance report response model."""
period_start: str
period_end: str
downloads_per_hour: float
average_queue_size: float
peak_memory_usage_mb: float
average_cpu_percent: float
uptime_seconds: float
error_rate: float
class SummaryReportResponse(BaseModel):
"""Comprehensive analytics summary response."""
timestamp: str
download_stats: DownloadStatsResponse
series_popularity: list[SeriesPopularityResponse]
storage_analysis: StorageAnalysisResponse
performance_report: PerformanceReportResponse
@router.get("/downloads", response_model=DownloadStatsResponse)
async def get_download_statistics(
days: int = 30,
db: AsyncSession = Depends(get_db_session),
) -> DownloadStatsResponse:
"""Get download statistics for specified period.
Args:
days: Number of days to analyze (default: 30)
db: Database session
Returns:
Download statistics including success rates and speeds
"""
try:
service = get_analytics_service()
stats = await service.get_download_stats(db, days=days)
return DownloadStatsResponse(
total_downloads=stats.total_downloads,
successful_downloads=stats.successful_downloads,
failed_downloads=stats.failed_downloads,
total_bytes_downloaded=stats.total_bytes_downloaded,
average_speed_mbps=stats.average_speed_mbps,
success_rate=stats.success_rate,
average_duration_seconds=stats.average_duration_seconds,
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get download statistics: {str(e)}",
)
@router.get(
"/series-popularity",
response_model=list[SeriesPopularityResponse]
)
async def get_series_popularity(
limit: int = 10,
db: AsyncSession = Depends(get_db_session),
) -> list[SeriesPopularityResponse]:
"""Get most popular series by download count.
Args:
limit: Maximum number of series (default: 10)
db: Database session
Returns:
List of series sorted by popularity
"""
try:
service = get_analytics_service()
popularity = await service.get_series_popularity(db, limit=limit)
return [
SeriesPopularityResponse(
series_name=p.series_name,
download_count=p.download_count,
total_size_bytes=p.total_size_bytes,
last_download=p.last_download,
success_rate=p.success_rate,
)
for p in popularity
]
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get series popularity: {str(e)}",
)
@router.get(
"/storage",
response_model=StorageAnalysisResponse
)
async def get_storage_analysis() -> StorageAnalysisResponse:
"""Get current storage usage analysis.
Returns:
Storage breakdown including disk and directory usage
"""
try:
service = get_analytics_service()
analysis = service.get_storage_analysis()
return StorageAnalysisResponse(
total_storage_bytes=analysis.total_storage_bytes,
used_storage_bytes=analysis.used_storage_bytes,
free_storage_bytes=analysis.free_storage_bytes,
storage_percent_used=analysis.storage_percent_used,
downloads_directory_size_bytes=(
analysis.downloads_directory_size_bytes
),
cache_directory_size_bytes=(
analysis.cache_directory_size_bytes
),
logs_directory_size_bytes=(
analysis.logs_directory_size_bytes
),
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get storage analysis: {str(e)}",
)
@router.get(
"/performance",
response_model=PerformanceReportResponse
)
async def get_performance_report(
hours: int = 24,
db: AsyncSession = Depends(get_db_session),
) -> PerformanceReportResponse:
"""Get performance metrics for specified period.
Args:
hours: Number of hours to analyze (default: 24)
db: Database session
Returns:
Performance metrics including speeds and system usage
"""
try:
service = get_analytics_service()
report = await service.get_performance_report(db, hours=hours)
return PerformanceReportResponse(
period_start=report.period_start,
period_end=report.period_end,
downloads_per_hour=report.downloads_per_hour,
average_queue_size=report.average_queue_size,
peak_memory_usage_mb=report.peak_memory_usage_mb,
average_cpu_percent=report.average_cpu_percent,
uptime_seconds=report.uptime_seconds,
error_rate=report.error_rate,
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get performance report: {str(e)}",
)
@router.get("/summary", response_model=SummaryReportResponse)
async def get_summary_report(
db: AsyncSession = Depends(get_db_session),
) -> SummaryReportResponse:
"""Get comprehensive analytics summary.
Args:
db: Database session
Returns:
Complete analytics report with all metrics
"""
try:
service = get_analytics_service()
summary = await service.generate_summary_report(db)
return SummaryReportResponse(
timestamp=summary["timestamp"],
download_stats=DownloadStatsResponse(
**summary["download_stats"]
),
series_popularity=[
SeriesPopularityResponse(**p)
for p in summary["series_popularity"]
],
storage_analysis=StorageAnalysisResponse(
**summary["storage_analysis"]
),
performance_report=PerformanceReportResponse(
**summary["performance_report"]
),
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to generate summary report: {str(e)}",
)