perf(api): свод 2 — новый режим страниц с total и эндпоинт вариантов фильтров

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-28 21:56:34 +04:00
parent e3a9a08e84
commit db28138b5b
2 changed files with 138 additions and 19 deletions

View File

@ -8,7 +8,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.api.v1.deps import get_current_user
from src.db.models.app_user import AppUser
from src.db.session import get_db
from src.domain.schemas import BaseListResponse, SheetResponse
from src.domain.schemas import (
BaseListResponse,
BaseSingleResponse,
SheetResponse,
Svod2Facets,
Svod2PageResponse,
)
from src.services.summary_service import SummaryService
@ -90,7 +96,33 @@ async def get_summary(
)
@router.get("/2/{sheet}/{year}")
def is_svod2_page_mode(
view: Optional[str],
search: Optional[str],
smeta_type: Optional[str],
smeta_direction: Optional[str],
org_name: Optional[str],
) -> bool:
return any(
value is not None
for value in (view, search, smeta_type, smeta_direction, org_name)
)
def _check_summary2_sheet(sheet: str) -> str:
sheet = sheet.upper()
if sheet not in VALID_SUMMARY2_SHEETS:
raise HTTPException(
404,
f"Лист свода {sheet} должен быть одним из: {', '.join(sorted(VALID_SUMMARY2_SHEETS))}",
)
return sheet
@router.get(
"/2/{sheet}/{year}",
response_model=Svod2PageResponse | BaseListResponse[SheetResponse],
)
async def get_summary2(
sheet: str,
year: int,
@ -100,28 +132,47 @@ async def get_summary2(
limit: Optional[int] = Query(default=None, ge=1),
sort_by: Literal["org_name", "section_code"] = "org_name",
sort_direction: Literal["asc", "desc"] = "asc",
view: Optional[Literal["full", "compact"]] = None,
search: Optional[str] = None,
smeta_type: Optional[str] = None,
smeta_direction: Optional[str] = None,
org_name: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(get_current_user),
) -> BaseListResponse[SheetResponse]:
sheet = sheet.upper()
if sheet not in VALID_SUMMARY2_SHEETS:
raise HTTPException(
404,
f"Лист свода {sheet} должен быть одним из: {', '.join(sorted(VALID_SUMMARY2_SHEETS))}",
)
) -> Svod2PageResponse | BaseListResponse[SheetResponse]:
sheet = _check_summary2_sheet(sheet)
page_mode = is_svod2_page_mode(view, search, smeta_type, smeta_direction, org_name)
t0 = time.perf_counter()
summary_service = SummaryService(db)
rows = await summary_service.get_svod2_rows(
year=year,
sheet=sheet,
user=current_user,
org_ids=_parse_org_ids(org_ids),
offset=offset,
limit=limit,
sort_by=sort_by,
sort_direction=sort_direction,
)
total = None
if page_mode:
rows, total = await summary_service.get_svod2_page(
year=year,
sheet=sheet,
user=current_user,
org_ids=_parse_org_ids(org_ids),
offset=offset,
limit=limit,
sort_by=sort_by,
sort_direction=sort_direction,
search=search,
smeta_type=smeta_type,
smeta_direction=smeta_direction,
org_name=org_name,
view=view or "full",
)
else:
rows = await summary_service.get_svod2_rows(
year=year,
sheet=sheet,
user=current_user,
org_ids=_parse_org_ids(org_ids),
offset=offset,
limit=limit,
sort_by=sort_by,
sort_direction=sort_direction,
)
db_ms = (time.perf_counter() - t0) * 1000
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
@ -136,11 +187,69 @@ async def get_summary2(
"limit": limit,
"sort_by": sort_by,
"sort_direction": sort_direction,
"view": view,
"search": search,
"smeta_type": smeta_type,
"smeta_direction": smeta_direction,
"org_name": org_name,
"user": current_user.id,
}
logger.info(f"db_time: {extra}", extra=extra)
if page_mode:
return Svod2PageResponse(
count=len(rows),
result=_rows_to_sheet_response(rows),
total=total,
)
return BaseListResponse(
count=len(rows),
result=_rows_to_sheet_response(rows),
)
@router.get("/2/{sheet}/{year}/facets")
async def get_summary2_facets(
sheet: str,
year: int,
response: Response,
org_ids: Optional[str] = None,
search: Optional[str] = None,
smeta_type: Optional[str] = None,
smeta_direction: Optional[str] = None,
org_name: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(get_current_user),
) -> BaseSingleResponse[Svod2Facets]:
sheet = _check_summary2_sheet(sheet)
t0 = time.perf_counter()
facets = await SummaryService(db).get_svod2_facets(
year=year,
sheet=sheet,
user=current_user,
org_ids=_parse_org_ids(org_ids),
search=search,
smeta_type=smeta_type,
smeta_direction=smeta_direction,
org_name=org_name,
)
db_ms = (time.perf_counter() - t0) * 1000
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
extra = {
"type": "db_time",
"time_ms": f"{db_ms:.2f}",
"handler": "get_summary2_facets",
"sheet": sheet,
"year": year,
"org_ids": org_ids,
"search": search,
"smeta_type": smeta_type,
"smeta_direction": smeta_direction,
"org_name": org_name,
"user": current_user.id,
}
logger.info(f"db_time: {extra}", extra=extra)
return BaseSingleResponse(result=facets)

View File

@ -0,0 +1,10 @@
import pytest
from src.api.v1.summary import is_svod2_page_mode
@pytest.mark.parametrize("args,expected", [
((None, None, None, None, None), False), (("full", None, None, None, None), True),
((None, "", None, None, None), True), ((None, None, None, None, "x"), True)])
def test_is_svod2_page_mode(args, expected):
assert is_svod2_page_mode(*args) is expected