"""Main app for backend api""" import logging import yaml import time from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, BackgroundTasks from .logging_config import configure_logging from .structs.landmark import Landmark from .structs.preferences import Preferences from .structs.linked_landmarks import LinkedLandmarks from .structs.trip import Trip from .overpass.overpass import fill_cache from .landmarks.landmarks_manager import LandmarkManager from .toilets.toilets_router import router as toilets_router from .optimization.optimization_router import router as optimization_router from .landmarks.landmarks_router import router as landmarks_router, get_landmarks_nearby from .optimization.optimizer import Optimizer from .optimization.refiner import Refiner from .cache import client as cache_client from .constants import OPTIMIZER_PARAMETERS_PATH 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) # Fetches the global list of landmarks given preferences and start/end coordinates. Two routes # Call with "/get/landmarks/" for main entry point of the trip generation pipeline. # Call with "/get-nearby/landmarks/" for the NEARBY feature. app.include_router(landmarks_router) # Optimizes the trip given preferences. Second step in the main trip generation pipeline # Call with "/optimize/trip" app.include_router(optimization_router) # Fetches toilets near given coordinates. # Call with "/get/toilets" for fetching toilets around coordinates app.include_router(toilets_router) ###### TO REMOVE ONCE THE FRONTEND IS UP TO DATE ###### @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") logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}") start_time = time.time() # Generate the landmarks from the start location landmarks = manager.generate_landmarks_list( center_coordinates = start, preferences = preferences ) 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() logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}") 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 ) # From the parameters load the length at which to truncate the landmarks list. with OPTIMIZER_PARAMETERS_PATH.open('r') as f: parameters = yaml.safe_load(f) n_important = parameters['N_important'] # Truncate to the most important landmarks for a shorter list landmarks_short = landmarks[:n_important] # insert start and finish to the shorter landmarks list landmarks_short.insert(0, start_landmark) landmarks_short.append(end_landmark) # First stage optimization try: base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) except Exception as exc: logger.error(f"Trip generation failed: {str(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 try : refined_tour = refiner.refine_optimization( landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute ) except Exception as exc : logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}") refined_tour = base_tour 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'Optimized a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_first_stage + t_second_stage,3)} seconds.') logger.info('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour)) 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: logger.error(f"Failed to fetch trip with UUID {trip_uuid}: {str(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: logger.error(f"Failed to fetch landmark with UUID {landmark_uuid}: {str(exc)}") raise HTTPException(status_code=404, detail="Landmark not found") from exc @app.post("/trip/recompute-time/{trip_uuid}/{removed_landmark_uuid}") def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip: """ Updates the reaching times of a given trip when removing a landmark. Args: landmark_uuid (str) : unique identifier for a Landmark. Returns: (Landmark) : the corresponding Landmark. """ # First, fetch the trip in the cache. try: trip = cache_client.get(f'trip_{trip_uuid}') except KeyError as exc: logger.error(f"Failed to update trip with UUID {trip_uuid} (trip not found): {str(exc)}") raise HTTPException(status_code=404, detail='Trip not found') from exc landmarks = [] next_uuid = trip.first_landmark_uuid # Extract landmarks try : while next_uuid is not None: landmark = cache_client.get(f'landmark_{next_uuid}') # Filter out the removed landmark. if next_uuid != removed_landmark_uuid : landmarks.append(landmark) next_uuid = landmark.next_uuid # Prepare for the next iteration except KeyError as exc: logger.error(f"Failed to update trip with UUID {trip_uuid} : {str(exc)}") raise HTTPException(status_code=404, detail=f'landmark {next_uuid} not found') from exc # Re-link every thing and compute times again linked_tour = LinkedLandmarks(landmarks) trip = Trip.from_linked_landmarks(linked_tour, cache_client) return trip