Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
207 lines
7.7 KiB
Python
207 lines
7.7 KiB
Python
"""Main app for backend api"""
|
|
|
|
import logging
|
|
import time
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI, HTTPException, BackgroundTasks, Query
|
|
|
|
from .logging_config import configure_logging
|
|
from .structs.landmark import Landmark, Toilets
|
|
from .structs.preferences import Preferences
|
|
from .structs.linked_landmarks import LinkedLandmarks
|
|
from .structs.trip import Trip
|
|
from .utils.landmarks_manager import LandmarkManager
|
|
from .utils.toilets_manager import ToiletsManager
|
|
from .optimization.optimizer import Optimizer
|
|
from .optimization.refiner import Refiner
|
|
from .overpass.overpass import fill_cache
|
|
from .cache import client as cache_client
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
manager = LandmarkManager()
|
|
optimizer = Optimizer()
|
|
refiner = Refiner(optimizer=optimizer)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Function to run at the start of the app"""
|
|
logger.info("Setting up logging")
|
|
configure_logging()
|
|
yield
|
|
logger.info("Shutting down logging")
|
|
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
|
|
|
|
@app.post("/trip/new")
|
|
def new_trip(preferences: Preferences,
|
|
start: tuple[float, float],
|
|
end: tuple[float, float] | None = None,
|
|
background_tasks: BackgroundTasks = None) -> Trip:
|
|
"""
|
|
Main function to call the optimizer.
|
|
|
|
Args:
|
|
preferences : the preferences specified by the user as the post body
|
|
start : the coordinates of the starting point
|
|
end : the coordinates of the finishing point
|
|
Returns:
|
|
(uuid) : The uuid of the first landmark in the optimized route
|
|
"""
|
|
if preferences is None:
|
|
raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
|
|
if (preferences.shopping.score == 0 and
|
|
preferences.sightseeing.score == 0 and
|
|
preferences.nature.score == 0) :
|
|
raise HTTPException(status_code=406, detail="All preferences are 0.")
|
|
if start is None:
|
|
raise HTTPException(status_code=406, detail="Start coordinates not provided")
|
|
if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
|
|
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
|
if end is None:
|
|
end = start
|
|
logger.info("No end coordinates provided. Using start=end.")
|
|
|
|
start_landmark = Landmark(name='start',
|
|
type='start',
|
|
location=(start[0], start[1]),
|
|
osm_type='start',
|
|
osm_id=0,
|
|
attractiveness=0,
|
|
duration=0,
|
|
must_do=True,
|
|
n_tags = 0)
|
|
|
|
end_landmark = Landmark(name='finish',
|
|
type='finish',
|
|
location=(end[0], end[1]),
|
|
osm_type='end',
|
|
osm_id=0,
|
|
attractiveness=0,
|
|
duration=0,
|
|
must_do=True,
|
|
n_tags=0)
|
|
|
|
start_time = time.time()
|
|
# Generate the landmarks from the start location
|
|
landmarks, landmarks_short = manager.generate_landmarks_list(
|
|
center_coordinates = start,
|
|
preferences = preferences
|
|
)
|
|
|
|
if len(landmarks) == 0 :
|
|
raise HTTPException(status_code=500, detail="No landmarks were found.")
|
|
|
|
# insert start and finish to the landmarks list
|
|
landmarks_short.insert(0, start_landmark)
|
|
landmarks_short.append(end_landmark)
|
|
|
|
t_generate_landmarks = time.time() - start_time
|
|
logger.info(f'Fetched {len(landmarks)} landmarks in \t: {round(t_generate_landmarks,3)} seconds')
|
|
start_time = time.time()
|
|
|
|
# First stage optimization
|
|
try:
|
|
base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc
|
|
|
|
t_first_stage = time.time() - start_time
|
|
start_time = time.time()
|
|
|
|
# Second stage optimization
|
|
# TODO : only if necessary (not enough landmarks for ex.)
|
|
try :
|
|
refined_tour = refiner.refine_optimization(landmarks, base_tour,
|
|
preferences.max_time_minute,
|
|
preferences.detour_tolerance_minute)
|
|
except TimeoutError as te :
|
|
logger.error(f'Refiner failed : {str(te)} Using base tour.')
|
|
refined_tour = base_tour
|
|
except Exception as exc :
|
|
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(exc)}") from exc
|
|
|
|
t_second_stage = time.time() - start_time
|
|
|
|
logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
|
|
logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds')
|
|
logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
|
|
|
|
linked_tour = LinkedLandmarks(refined_tour)
|
|
# upon creation of the trip, persistence of both the trip and its landmarks is ensured.
|
|
trip = Trip.from_linked_landmarks(linked_tour, cache_client)
|
|
logger.info(f'Generated a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_generate_landmarks + t_first_stage + t_second_stage,3)} seconds.')
|
|
|
|
background_tasks.add_task(fill_cache)
|
|
|
|
return trip
|
|
|
|
|
|
#### For already existing trips/landmarks
|
|
@app.get("/trip/{trip_uuid}")
|
|
def get_trip(trip_uuid: str) -> Trip:
|
|
"""
|
|
Look-up the cache for a trip that has been previously generated using its identifier.
|
|
|
|
Args:
|
|
trip_uuid (str) : unique identifier for a trip.
|
|
|
|
Returns:
|
|
(Trip) : the corresponding trip.
|
|
"""
|
|
try:
|
|
trip = cache_client.get(f"trip_{trip_uuid}")
|
|
return trip
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="Trip not found") from exc
|
|
|
|
|
|
@app.get("/landmark/{landmark_uuid}")
|
|
def get_landmark(landmark_uuid: str) -> Landmark:
|
|
"""
|
|
Returns a Landmark from its unique identifier.
|
|
|
|
Args:
|
|
landmark_uuid (str) : unique identifier for a Landmark.
|
|
|
|
Returns:
|
|
(Landmark) : the corresponding Landmark.
|
|
"""
|
|
try:
|
|
landmark = cache_client.get(f"landmark_{landmark_uuid}")
|
|
return landmark
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="Landmark not found") from exc
|
|
|
|
|
|
@app.post("/toilets/new")
|
|
def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] :
|
|
"""
|
|
Endpoint to find toilets within a specified radius from a given location.
|
|
|
|
This endpoint expects the `location` and `radius` as **query parameters**, not in the request body.
|
|
|
|
Args:
|
|
location (tuple[float, float]): The latitude and longitude of the location to search from.
|
|
radius (int, optional): The radius (in meters) within which to search for toilets. Defaults to 500 meters.
|
|
|
|
Returns:
|
|
list[Toilets]: A list of Toilets objects that meet the criteria.
|
|
"""
|
|
if location is None:
|
|
raise HTTPException(status_code=406, detail="Coordinates not provided or invalid")
|
|
if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180):
|
|
raise HTTPException(status_code=422, detail="Start coordinates not in range")
|
|
|
|
toilets_manager = ToiletsManager(location, radius)
|
|
|
|
try :
|
|
toilets_list = toilets_manager.generate_toilet_list()
|
|
return toilets_list
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="No toilets found") from exc
|