"""Read-only API для location (#948 Part B, ТЗ §8.2). GET /api/v1/locations → LocationList (все локации с индексами) GET /api/v1/locations/{district_name} → LocationOut (одна; 404 если нет) Read-only: индексы инфра/конкуренция/спрос/будущее-предложение ВЫЧИСЛЯЮТСЯ воркером (tasks/location_refresh.py через services/site_finder/locations.py), НЕ вводятся пользователем — поэтому CRUD нет (никаких POST/PUT/DELETE). Access control: backend rbac_guard (app/main.py) хард-блокирует ТОЛЬКО /api/v1/admin/* (admin-only). На /api/v1/locations pilot отсекается FRONTEND-овым RouteGuard (allowed_paths из /me) + Caddy, НЕ бэкендом (is_path_allowed() из backend-кода тут НЕ вызывается). До эндпоинта доходят analyst+admin (та же модель, что insights/ custom-pois — НЕ backend pilot-gating). Хэндлеры sync `def` (зеркало insights.py/custom_pois.py): сервис ходит в БД через sync SQLAlchemy Session → FastAPI исполняет их в threadpool, event loop не блокируется (async def + sync db.execute было бы хуже — заблокировало бы loop). district_name в path: FastAPI принимает Cyrillic через URL-encoding прозрачно (имена 8 админ-районов ЕКБ: «Кировский», «Ленинский», …). """ from __future__ import annotations import logging from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app.core.db import get_db from app.schemas.location import LocationList, LocationOut from app.services.site_finder.locations import get_location, list_locations logger = logging.getLogger(__name__) router = APIRouter() @router.get("", response_model=LocationList) def list_locations_endpoint( db: Annotated[Session, Depends(get_db)], ) -> Any: """Все локации (районы) с district-level индексами.""" return list_locations(db) @router.get("/{district_name}", response_model=LocationOut) def get_location_endpoint( district_name: str, db: Annotated[Session, Depends(get_db)], ) -> Any: """Вернуть одну локацию по district_name. 404 если не найдена.""" location = get_location(db, district_name) if location is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Location not found") return location