Add POI API routes and Celery task
FastAPI router with CRUD endpoints for POIs, distance calculation trigger, and distance queries. Streaming GeoJSON endpoint now accepts include_poi_distances=true to inject travel times into features. Celery task wraps the distance calculator with progress reporting.
This commit is contained in:
parent
da0a56895d
commit
bd788df9aa
4 changed files with 332 additions and 3 deletions
200
api/poi_routes.py
Normal file
200
api/poi_routes.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api.auth import User, get_current_user
|
||||
from database import engine
|
||||
from models.listing import ListingType
|
||||
from repositories.poi_repository import POIRepository
|
||||
from repositories.user_repository import UserRepository
|
||||
from services import poi_service, task_service
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
poi_router = APIRouter(prefix="/api/poi", tags=["poi"])
|
||||
|
||||
|
||||
class CreatePOIRequest(BaseModel):
|
||||
name: str
|
||||
address: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
|
||||
|
||||
class UpdatePOIRequest(BaseModel):
|
||||
name: str | None = None
|
||||
address: str | None = None
|
||||
latitude: float | None = None
|
||||
longitude: float | None = None
|
||||
|
||||
|
||||
class POIResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
address: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
created_at: str
|
||||
|
||||
|
||||
class CalculateRequest(BaseModel):
|
||||
travel_modes: list[str] # WALK, BICYCLE, TRANSIT
|
||||
listing_type: ListingType = ListingType.RENT
|
||||
listing_ids: list[int] | None = None
|
||||
|
||||
|
||||
class POIDistanceResponse(BaseModel):
|
||||
poi_id: int
|
||||
poi_name: str
|
||||
travel_mode: str
|
||||
duration_seconds: int
|
||||
distance_meters: int
|
||||
|
||||
|
||||
def _get_user_id(user: User) -> int:
|
||||
"""Resolve auth User to database user ID."""
|
||||
user_repo = UserRepository(engine)
|
||||
db_user = user_repo.get_user_by_email(user.email)
|
||||
if db_user is None:
|
||||
# Auto-create user on first POI interaction
|
||||
db_user = user_repo.create_user(user.email)
|
||||
assert db_user.id is not None
|
||||
return db_user.id
|
||||
|
||||
|
||||
def _poi_to_response(poi: "poi_service.PointOfInterest") -> POIResponse:
|
||||
return POIResponse(
|
||||
id=poi.id, # type: ignore[arg-type]
|
||||
name=poi.name,
|
||||
address=poi.address,
|
||||
latitude=poi.latitude,
|
||||
longitude=poi.longitude,
|
||||
created_at=poi.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
@poi_router.get("", response_model=list[POIResponse])
|
||||
async def list_pois(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
) -> list[POIResponse]:
|
||||
"""List all POIs for the current user."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = POIRepository(engine)
|
||||
pois = poi_service.get_user_pois(repo, user_id)
|
||||
return [_poi_to_response(p) for p in pois]
|
||||
|
||||
|
||||
@poi_router.post("", response_model=POIResponse)
|
||||
async def create_poi(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
body: CreatePOIRequest,
|
||||
) -> POIResponse:
|
||||
"""Create a new POI."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = POIRepository(engine)
|
||||
result = poi_service.create_poi(
|
||||
repo,
|
||||
user_id=user_id,
|
||||
name=body.name,
|
||||
address=body.address,
|
||||
latitude=body.latitude,
|
||||
longitude=body.longitude,
|
||||
)
|
||||
return _poi_to_response(result.poi)
|
||||
|
||||
|
||||
@poi_router.put("/{poi_id}", response_model=POIResponse)
|
||||
async def update_poi(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
poi_id: int,
|
||||
body: UpdatePOIRequest,
|
||||
) -> POIResponse:
|
||||
"""Update an existing POI."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = POIRepository(engine)
|
||||
result = poi_service.update_poi(
|
||||
repo,
|
||||
poi_id=poi_id,
|
||||
user_id=user_id,
|
||||
name=body.name,
|
||||
address=body.address,
|
||||
latitude=body.latitude,
|
||||
longitude=body.longitude,
|
||||
)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="POI not found")
|
||||
return _poi_to_response(result.poi)
|
||||
|
||||
|
||||
@poi_router.delete("/{poi_id}")
|
||||
async def delete_poi(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
poi_id: int,
|
||||
) -> dict[str, bool]:
|
||||
"""Delete a POI and its associated distances."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = POIRepository(engine)
|
||||
deleted = poi_service.delete_poi(repo, poi_id, user_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="POI not found")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@poi_router.post("/{poi_id}/calculate")
|
||||
async def calculate_distances(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
poi_id: int,
|
||||
body: CalculateRequest,
|
||||
) -> dict[str, str]:
|
||||
"""Trigger distance calculation for a POI."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = POIRepository(engine)
|
||||
|
||||
# Verify POI exists and belongs to user
|
||||
poi = poi_service.get_poi(repo, poi_id)
|
||||
if poi is None or poi.user_id != user_id:
|
||||
raise HTTPException(status_code=404, detail="POI not found")
|
||||
|
||||
result = poi_service.trigger_calculation(
|
||||
poi_id=poi_id,
|
||||
travel_modes=body.travel_modes,
|
||||
listing_type=body.listing_type,
|
||||
user_email=user.email,
|
||||
listing_ids=body.listing_ids,
|
||||
)
|
||||
|
||||
if result.task_id:
|
||||
task_service.add_task_for_user(user.email, result.task_id)
|
||||
|
||||
return {"task_id": result.task_id or "", "message": result.message}
|
||||
|
||||
|
||||
@poi_router.get("/distances")
|
||||
async def get_distances(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
listing_id: int,
|
||||
listing_type: ListingType = ListingType.RENT,
|
||||
) -> list[POIDistanceResponse]:
|
||||
"""Get POI distances for a specific listing."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = POIRepository(engine)
|
||||
poi_repo_pois = {
|
||||
p.id: p for p in poi_service.get_user_pois(repo, user_id)
|
||||
}
|
||||
|
||||
distances = poi_service.get_distances_for_listing(
|
||||
repo, listing_id, listing_type, user_id
|
||||
)
|
||||
|
||||
return [
|
||||
POIDistanceResponse(
|
||||
poi_id=d.poi_id,
|
||||
poi_name=poi_repo_pois[d.poi_id].name if d.poi_id in poi_repo_pois else "Unknown",
|
||||
travel_mode=d.travel_mode,
|
||||
duration_seconds=d.duration_seconds,
|
||||
distance_meters=d.distance_meters,
|
||||
)
|
||||
for d in distances
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue