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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user