refactor: FastAPI best practices — return types, Pydantic schemas, middleware, code dedup

- Все 18 роутов получили return type annotations
- Создан schemas.py с Pydantic-моделями (ScanOut, PackageOut, FindingOut, ...)
- API-роуты: response_model на list/detail/export/stats
- 404 через HTTPException(404) вместо {'detail':'Not found'} (200)
- RequestLoggingMiddleware: method, path, status, duration_ms
- Глобальный exception handler: ловит необработанные исключения → 500
- _parse_flagged(): вынесен дублирующийся string→bool
- parse_package_path(): общий для web.py и api_packages.py
- selectinload: вынесены в top-level imports
- harvester: makedirs/mkdtemp/rmtree обёрнуты в asyncio.to_thread()
This commit is contained in:
Marker689
2026-05-10 12:53:33 +03:00
parent 935d96b35a
commit c1258dde99
11 changed files with 188 additions and 55 deletions

View File

@@ -3,7 +3,7 @@
import csv
import io
from fastapi import APIRouter, Depends, Query, Response
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -19,11 +19,12 @@ from ..constants import (
from ..db.engine import get_session
from ..db.models import Scan
from ..db.queries import build_scan_list_query, get_dashboard_stats
from ..schemas import ScanDetailOut, ScanListResponse, StatsResponse
router = APIRouter(prefix="/api/v1/scans", tags=["scans"])
@router.get("")
@router.get("", response_model=ScanListResponse)
async def list_scans(
limit: int = Query(DEFAULT_PAGE_SIZE, le=MAX_PAGE_SIZE),
offset: int = Query(DEFAULT_OFFSET, ge=0),
@@ -34,7 +35,7 @@ async def list_scans(
sort_by: str = Query(DEFAULT_SORT_BY_SCANS),
sort_dir: str = Query(DEFAULT_SORT_DIR),
session: AsyncSession = Depends(get_session),
):
) -> dict:
q, count_q = build_scan_list_query(
flagged=flagged,
status=status,
@@ -77,7 +78,7 @@ async def export_scans_csv(
search: str | None = Query(None),
status: str | None = Query(None),
session: AsyncSession = Depends(get_session),
):
) -> Response:
q, _count_q = build_scan_list_query(
flagged=flagged,
status=status,
@@ -132,8 +133,8 @@ async def export_scans_csv(
)
@router.get("/stats")
async def scan_stats(session: AsyncSession = Depends(get_session)):
@router.get("/stats", response_model=StatsResponse)
async def scan_stats(session: AsyncSession = Depends(get_session)) -> dict:
dashboard = await get_dashboard_stats(session)
return {
"total_scans": dashboard["total_scans"],
@@ -147,13 +148,13 @@ async def scan_stats(session: AsyncSession = Depends(get_session)):
}
@router.get("/{scan_id}")
async def get_scan(scan_id: int, session: AsyncSession = Depends(get_session)):
@router.get("/{scan_id}", response_model=ScanDetailOut)
async def get_scan(scan_id: int, session: AsyncSession = Depends(get_session)) -> dict:
scan = await session.scalar(
select(Scan).where(Scan.id == scan_id).options(selectinload(Scan.findings))
)
if not scan:
return {"detail": "Not found"}
raise HTTPException(status_code=404, detail="Scan not found")
return {
"id": scan.id,
"package_name": scan.package_name,