Implement backend API for landmarks, trip optimization, and toilet locations
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m49s
				
			
		
			
				
	
				Run linting on the backend code / Build (pull_request) Successful in 30s
				
			
		
			
				
	
				Run testing on the backend code / Build (pull_request) Failing after 45s
				
			
		
			
				
	
				Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 32s
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build and deploy the backend to staging / Build and push image (pull_request) Successful in 1m49s
				
			Run linting on the backend code / Build (pull_request) Successful in 30s
				
			Run testing on the backend code / Build (pull_request) Failing after 45s
				
			Build and deploy the backend to staging / Deploy to staging (pull_request) Successful in 32s
				
			- Added landmarks_router.py to handle landmark retrieval based on user preferences and location. - Implemented optimization_router.py for trip optimization, including handling preferences and landmarks. - Created toilets_router.py to fetch toilet locations within a specified radius from a given location. - Enhanced error handling and logging across all new endpoints. - Generated a comprehensive report.html for test results and environment details.
This commit is contained in:
		
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -4,7 +4,6 @@ import yaml | |||||||
|  |  | ||||||
| from ..structs.preferences import Preferences | from ..structs.preferences import Preferences | ||||||
| from ..structs.landmark import Landmark | from ..structs.landmark import Landmark | ||||||
| from ..utils.take_most_important import take_most_important |  | ||||||
| from .cluster_manager import ClusterManager | from .cluster_manager import ClusterManager | ||||||
| from ..overpass.overpass import Overpass, get_base_info | from ..overpass.overpass import Overpass, get_base_info | ||||||
| from ..utils.bbox import create_bbox | from ..utils.bbox import create_bbox | ||||||
| @@ -23,7 +22,7 @@ class LandmarkManager: | |||||||
|     church_coeff: float     # coeff to adjsut score of churches |     church_coeff: float     # coeff to adjsut score of churches | ||||||
|     nature_coeff: float       # coeff to adjust score of parks |     nature_coeff: float       # coeff to adjust score of parks | ||||||
|     overall_coeff: float        # coeff to adjust weight of tags |     overall_coeff: float        # coeff to adjust weight of tags | ||||||
|     n_important: int        # number of important landmarks to consider |     # n_important: int        # number of important landmarks to consider | ||||||
|  |  | ||||||
|  |  | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
| @@ -42,7 +41,7 @@ class LandmarkManager: | |||||||
|             self.wikipedia_bonus = parameters['wikipedia_bonus'] |             self.wikipedia_bonus = parameters['wikipedia_bonus'] | ||||||
|             self.viewpoint_bonus = parameters['viewpoint_bonus'] |             self.viewpoint_bonus = parameters['viewpoint_bonus'] | ||||||
|             self.pay_bonus = parameters['pay_bonus'] |             self.pay_bonus = parameters['pay_bonus'] | ||||||
|             self.n_important = parameters['N_important'] |             # self.n_important = parameters['N_important'] | ||||||
|  |  | ||||||
|         with OPTIMIZER_PARAMETERS_PATH.open('r') as f: |         with OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||||
|             parameters = yaml.safe_load(f) |             parameters = yaml.safe_load(f) | ||||||
| @@ -60,7 +59,7 @@ class LandmarkManager: | |||||||
|             center_coordinates: tuple[float, float], |             center_coordinates: tuple[float, float], | ||||||
|             preferences: Preferences, |             preferences: Preferences, | ||||||
|             allow_clusters: bool = True |             allow_clusters: bool = True | ||||||
|             ) -> tuple[list[Landmark], list[Landmark]]: |             ) -> list[Landmark] : | ||||||
|         """ |         """ | ||||||
|         Generate and prioritize a list of landmarks based on user preferences. |         Generate and prioritize a list of landmarks based on user preferences. | ||||||
|  |  | ||||||
| @@ -127,13 +126,12 @@ class LandmarkManager: | |||||||
|                 all_landmarks.update(shopping_clusters) |                 all_landmarks.update(shopping_clusters) | ||||||
|  |  | ||||||
|  |  | ||||||
|         landmarks_constrained = take_most_important(all_landmarks, self.n_important) |  | ||||||
|  |  | ||||||
|         # DETAILS HERE |         # DETAILS HERE | ||||||
|         # self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.') |         # self.logger.info(f'All landmarks generated : {len(all_landmarks)} landmarks around {center_coordinates}, and constrained to {len(landmarks_constrained)} most important ones.') | ||||||
|         self.logger.info(f'Found {len(all_landmarks)} landmarks in total.') |         self.logger.info(f'Found {len(all_landmarks)} landmarks in total.') | ||||||
|  |  | ||||||
|         return all_landmarks, landmarks_constrained |         return sorted(all_landmarks, key=lambda x: x.attractiveness, reverse=True) | ||||||
|  |      | ||||||
|  |  | ||||||
|     def set_landmark_score(self, landmark: Landmark, landmarktype: str, preference_level: int) : |     def set_landmark_score(self, landmark: Landmark, landmarktype: str, preference_level: int) : | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ from fastapi import HTTPException, APIRouter | |||||||
| 
 | 
 | ||||||
| from ..structs.landmark import Landmark | from ..structs.landmark import Landmark | ||||||
| from ..structs.preferences import Preferences, Preference | from ..structs.preferences import Preferences, Preference | ||||||
| from ..landmarks.landmarks_manager import LandmarkManager | from .landmarks_manager import LandmarkManager | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Setup the logger and the Landmarks Manager | # Setup the logger and the Landmarks Manager | ||||||
| @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) | |||||||
| manager = LandmarkManager() | manager = LandmarkManager() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Start the router | # Initialize the API router | ||||||
| router = APIRouter() | router = APIRouter() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -48,7 +48,7 @@ def get_landmarks( | |||||||
|     start_time = time.time() |     start_time = time.time() | ||||||
| 
 | 
 | ||||||
|     # Generate the landmarks from the start location |     # Generate the landmarks from the start location | ||||||
|     landmarks, _ = manager.generate_landmarks_list( |     landmarks = manager.generate_landmarks_list( | ||||||
|         center_coordinates = start, |         center_coordinates = start, | ||||||
|         preferences = preferences |         preferences = preferences | ||||||
|     ) |     ) | ||||||
| @@ -62,7 +62,7 @@ def get_landmarks( | |||||||
|     return landmarks |     return landmarks | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @router.post("/landmarks/get-nearby/{lat}/{lon}") | @router.post("/get-nearby/landmarks/{lat}/{lon}") | ||||||
| def get_landmarks_nearby( | def get_landmarks_nearby( | ||||||
|     lat: float, |     lat: float, | ||||||
|     lon: float |     lon: float | ||||||
| @@ -81,6 +81,7 @@ def get_landmarks_nearby( | |||||||
|         list[Landmark]: A list of selected nearby landmarks. |         list[Landmark]: A list of selected nearby landmarks. | ||||||
|     """ |     """ | ||||||
|     logger.info(f'Fetching landmarks nearby ({lat}, {lon}).') |     logger.info(f'Fetching landmarks nearby ({lat}, {lon}).') | ||||||
|  | 
 | ||||||
|     # Define fixed preferences: |     # Define fixed preferences: | ||||||
|     prefs = Preferences( |     prefs = Preferences( | ||||||
|         sightseeing = Preference( |         sightseeing = Preference( | ||||||
| @@ -100,18 +101,23 @@ def get_landmarks_nearby( | |||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     # Find the landmarks around the location |     # Find the landmarks around the location | ||||||
|     _, landmarks_around = manager.generate_landmarks_list( |     landmarks_around = manager.generate_landmarks_list( | ||||||
|         center_coordinates = (lat, lon), |         center_coordinates = (lat, lon), | ||||||
|         preferences = prefs, |         preferences = prefs, | ||||||
|         allow_clusters=False, |         allow_clusters=False, | ||||||
|     ) |     ) | ||||||
|  | 
 | ||||||
|     if len(landmarks_around) == 0 : |     if len(landmarks_around) == 0 : | ||||||
|         raise HTTPException(status_code=500, detail="No landmarks were found.") |         raise HTTPException(status_code=500, detail="No landmarks were found.") | ||||||
| 
 | 
 | ||||||
|     # select 5 landmarks from there |     # select 8 - 12 landmarks from there | ||||||
|     if len(landmarks_around) > 6 : |     if len(landmarks_around) > 8 : | ||||||
|         landmarks_around = landmarks_around[:2] + random.sample(landmarks_around[3:], 2) |         n_imp = random.randint(2,5) | ||||||
|  |         rest = random.randint(8 - n_imp, min(12, len(landmarks_around))-n_imp) | ||||||
| 
 | 
 | ||||||
|     logger.info(f'Found {len(landmarks_around)} landmarks nearby ({lat}, {lon}).') |         print(f'len = {len(landmarks_around)}\nn_imp = {n_imp}\nrest = {rest}') | ||||||
|     logger.debug('Suggested landmarks :\n\t' + '\n\t'.join(f'{landmark}' for landmark in landmarks_around)) |         landmarks_around = landmarks_around[:n_imp] + random.sample(landmarks_around[n_imp:], rest) | ||||||
|  |      | ||||||
|  |     logger.info(f'Found {len(landmarks_around)} landmarks to suggest nearby ({lat}, {lon}).') | ||||||
|  |     # logger.debug('Suggested landmarks :\n\t' + '\n\t'.join(f'{landmark}' for landmark in landmarks_around)) | ||||||
|     return landmarks_around |     return landmarks_around | ||||||
| @@ -1,22 +1,18 @@ | |||||||
| """Main app for backend api""" | """Main app for backend api""" | ||||||
| import logging | import logging | ||||||
| import time |  | ||||||
| import random |  | ||||||
| from contextlib import asynccontextmanager | from contextlib import asynccontextmanager | ||||||
| from fastapi import FastAPI, HTTPException, BackgroundTasks | from fastapi import FastAPI, HTTPException | ||||||
|  |  | ||||||
| from .logging_config import configure_logging | from .logging_config import configure_logging | ||||||
| from .structs.landmark import Landmark | from .structs.landmark import Landmark | ||||||
| from .structs.preferences import Preferences, Preference |  | ||||||
| from .structs.linked_landmarks import LinkedLandmarks | from .structs.linked_landmarks import LinkedLandmarks | ||||||
| from .structs.trip import Trip | from .structs.trip import Trip | ||||||
| from .landmarks.landmarks_manager import LandmarkManager | from .landmarks.landmarks_manager import LandmarkManager | ||||||
| from .toilets.toilets_route import router as toilets_router | from .toilets.toilets_router import router as toilets_router | ||||||
| from .optimization.optimization_routes import router as optimization_router | from .optimization.optimization_router import router as optimization_router | ||||||
| from .landmarks.landmarks_routes import router as landmarks_router | from .landmarks.landmarks_router import router as landmarks_router, get_landmarks_nearby | ||||||
| from .optimization.optimizer import Optimizer | from .optimization.optimizer import Optimizer | ||||||
| from .optimization.refiner import Refiner | from .optimization.refiner import Refiner | ||||||
| from .overpass.overpass import fill_cache |  | ||||||
| from .cache import client as cache_client | from .cache import client as cache_client | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -40,118 +36,21 @@ app = FastAPI(lifespan=lifespan) | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app.include_router(toilets_router) | # Fetches the global list of landmarks given preferences and start/end coordinates. Two routes | ||||||
| app.include_router(optimization_router) | # 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) | app.include_router(landmarks_router) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.post("/trip/new") | # Optimizes the trip given preferences. Second step in the main trip generation pipeline | ||||||
| def new_trip( | # Call with "/optimize/trip" | ||||||
|     preferences: Preferences, | app.include_router(optimization_router) | ||||||
|     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.") |  | ||||||
|  |  | ||||||
|     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}") | # Fetches toilets near given coordinates. | ||||||
|  | # Call with "/get/toilets" for fetching toilets around coordinates | ||||||
|  | app.include_router(toilets_router) | ||||||
|  |  | ||||||
|     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: |  | ||||||
|         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 |  | ||||||
|     # 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 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'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.') |  | ||||||
|     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 | #### For already existing trips/landmarks | ||||||
|   | |||||||
| @@ -1,19 +1,20 @@ | |||||||
| """Main app for backend api""" | """API entry point for the trip optimization.""" | ||||||
|  | 
 | ||||||
| import logging | import logging | ||||||
| import time | import time | ||||||
| import yaml | import yaml | ||||||
| from fastapi import HTTPException, APIRouter, BackgroundTasks | from fastapi import HTTPException, APIRouter, BackgroundTasks | ||||||
| 
 | 
 | ||||||
|  | from .optimizer import Optimizer | ||||||
|  | from .refiner import Refiner | ||||||
| from ..structs.landmark import Landmark | from ..structs.landmark import Landmark | ||||||
| from ..structs.preferences import Preferences | from ..structs.preferences import Preferences | ||||||
| from ..structs.linked_landmarks import LinkedLandmarks | from ..structs.linked_landmarks import LinkedLandmarks | ||||||
| from ..utils.take_most_important import take_most_important | from ..utils.take_most_important import take_most_important | ||||||
| from ..structs.trip import Trip | from ..structs.trip import Trip | ||||||
| from ..optimization.optimizer import Optimizer |  | ||||||
| from ..optimization.refiner import Refiner |  | ||||||
| from ..overpass.overpass import fill_cache | from ..overpass.overpass import fill_cache | ||||||
| from ..cache import client as cache_client | from ..cache import client as cache_client | ||||||
| from ..constants import LANDMARK_PARAMETERS_PATH | from ..constants import OPTIMIZER_PARAMETERS_PATH | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Setup the Logger, Optimizer and Refiner | # Setup the Logger, Optimizer and Refiner | ||||||
| @@ -22,7 +23,7 @@ optimizer = Optimizer() | |||||||
| refiner = Refiner(optimizer=optimizer) | refiner = Refiner(optimizer=optimizer) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Start the router | # Initialize the API router | ||||||
| router = APIRouter() | router = APIRouter() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -66,7 +67,8 @@ def optimize_trip( | |||||||
| 
 | 
 | ||||||
|     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}") |     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', |     start_landmark = Landmark( | ||||||
|  |         name='start', | ||||||
|         type='start', |         type='start', | ||||||
|         location=(start[0], start[1]), |         location=(start[0], start[1]), | ||||||
|         osm_type='start', |         osm_type='start', | ||||||
| @@ -74,9 +76,11 @@ def optimize_trip( | |||||||
|         attractiveness=0, |         attractiveness=0, | ||||||
|         duration=0, |         duration=0, | ||||||
|         must_do=True, |         must_do=True, | ||||||
|                               n_tags = 0) |         n_tags = 0 | ||||||
|  |     ) | ||||||
| 
 | 
 | ||||||
|     end_landmark = Landmark(name='finish', |     end_landmark = Landmark( | ||||||
|  |         name='finish', | ||||||
|         type='finish', |         type='finish', | ||||||
|         location=(end[0], end[1]), |         location=(end[0], end[1]), | ||||||
|         osm_type='end', |         osm_type='end', | ||||||
| @@ -84,10 +88,11 @@ def optimize_trip( | |||||||
|         attractiveness=0, |         attractiveness=0, | ||||||
|         duration=0, |         duration=0, | ||||||
|         must_do=True, |         must_do=True, | ||||||
|                             n_tags=0) |         n_tags=0 | ||||||
|  |     ) | ||||||
|      |      | ||||||
|     # From the parameters load the length at which to truncate the landmarks list. |     # From the parameters load the length at which to truncate the landmarks list. | ||||||
|     with LANDMARK_PARAMETERS_PATH.open('r') as f: |     with OPTIMIZER_PARAMETERS_PATH.open('r') as f: | ||||||
|             parameters = yaml.safe_load(f) |             parameters = yaml.safe_load(f) | ||||||
|             n_important = parameters['N_important'] |             n_important = parameters['N_important'] | ||||||
| 
 | 
 | ||||||
| @@ -7,5 +7,4 @@ tag_exponent: 1.15 | |||||||
| image_bonus: 1.1 | image_bonus: 1.1 | ||||||
| viewpoint_bonus: 10 | viewpoint_bonus: 10 | ||||||
| wikipedia_bonus: 1.25 | wikipedia_bonus: 1.25 | ||||||
| N_important: 60 |  | ||||||
| pay_bonus: -1 | pay_bonus: -1 | ||||||
|   | |||||||
| @@ -7,3 +7,4 @@ overshoot: 0.0016 | |||||||
| time_limit: 1 | time_limit: 1 | ||||||
| gap_rel: 0.025 | gap_rel: 0.025 | ||||||
| max_iter: 80 | max_iter: 80 | ||||||
|  | N_important: 60 | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Defines the Preferences used as input for trip generation.""" | """Defines the Preferences used as input for trip generation.""" | ||||||
|  |  | ||||||
| from typing import Optional, Literal | from typing import Optional, Literal | ||||||
| from pydantic import BaseModel | from pydantic import BaseModel, field_validator | ||||||
|  |  | ||||||
|  |  | ||||||
| class Preference(BaseModel) : | class Preference(BaseModel) : | ||||||
| @@ -15,6 +15,13 @@ class Preference(BaseModel) : | |||||||
|     type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] |     type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] | ||||||
|     score: int          # score could be from 1 to 5 |     score: int          # score could be from 1 to 5 | ||||||
|  |  | ||||||
|  |     @field_validator("type") | ||||||
|  |     @classmethod | ||||||
|  |     def validate_type(cls, v): | ||||||
|  |         if v not in {'sightseeing', 'nature', 'shopping', 'start', 'finish'}: | ||||||
|  |             raise ValueError(f"Invalid type: {v}") | ||||||
|  |         return v | ||||||
|  |  | ||||||
|  |  | ||||||
| # Input for optimization | # Input for optimization | ||||||
| class Preferences(BaseModel) : | class Preferences(BaseModel) : | ||||||
|   | |||||||
| @@ -19,15 +19,35 @@ def invalid_client(): | |||||||
|         ([48.8566, 2.3522], {}, 422), |         ([48.8566, 2.3522], {}, 422), | ||||||
|  |  | ||||||
|         # Invalid cases: incomplete preferences. |         # Invalid cases: incomplete preferences. | ||||||
|         ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no shopping |         ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5},    # no shopping pref | ||||||
|                                  "nature": {"type": "nature", "score": 5}, |                                  "nature": {"type": "nature", "score": 5}, | ||||||
|                                  }, 422), |                                  }, 422), | ||||||
|         ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5}, # no nature |         ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 5},    # no nature pref | ||||||
|                                  "shopping": {"type": "shopping", "score": 5}, |                                  "shopping": {"type": "shopping", "score": 5}, | ||||||
|                                  }, 422), |                                  }, 422), | ||||||
|         ([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5},      # no sightseeing |         ([48.084588, 7.280405], {"nature": {"type": "nature", "score": 5},              # no sightseeing pref | ||||||
|                                  "shopping": {"type": "shopping", "score": 5}, |                                  "shopping": {"type": "shopping", "score": 5}, | ||||||
|                                  }, 422), |                                  }, 422), | ||||||
|  |         ([48.084588, 7.280405], {"sightseeing": {"type": "nature", "score": 1},         # mixed up preferences types. TODO: i suggest reducing the complexity by remove the Preference object. | ||||||
|  |                                  "nature": {"type": "shopping", "score": 1}, | ||||||
|  |                                  "shopping": {"type": "shopping", "score": 1}, | ||||||
|  |                                  }, 422), | ||||||
|  |         ([48.084588, 7.280405], {"doesnotexist": {"type": "sightseeing", "score": 2},   # non-existing preferences types | ||||||
|  |                                  "nature": {"type": "nature", "score": 2}, | ||||||
|  |                                  "shopping": {"type": "shopping", "score": 2}, | ||||||
|  |                                  }, 422), | ||||||
|  |         ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 3},    # non-existing preferences types | ||||||
|  |                                  "nature": {"type": "doesntexisteither", "score": 3}, | ||||||
|  |                                  "shopping": {"type": "shopping", "score": 3}, | ||||||
|  |                                  }, 422), | ||||||
|  |         ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": -1},   # negative preference value | ||||||
|  |                                  "nature": {"type": "doesntexisteither", "score": 4}, | ||||||
|  |                                  "shopping": {"type": "shopping", "score": 4}, | ||||||
|  |                                  }, 422), | ||||||
|  |         ([48.084588, 7.280405], {"sightseeing": {"type": "sightseeing", "score": 10},   # too high preference value | ||||||
|  |                                  "nature": {"type": "doesntexisteither", "score": 4}, | ||||||
|  |                                  "shopping": {"type": "shopping", "score": 4}, | ||||||
|  |                                  }, 422), | ||||||
|  |  | ||||||
|         # Invalid cases: unexisting coords |         # Invalid cases: unexisting coords | ||||||
|         ([91, 181], {"sightseeing": {"type": "sightseeing", "score": 5}, |         ([91, 181], {"sightseeing": {"type": "sightseeing", "score": 5}, | ||||||
| @@ -53,7 +73,7 @@ def test_input(invalid_client, start, preferences, status_code):   # pylint: dis | |||||||
|     Test new trip creation with different sets of preferences and locations. |     Test new trip creation with different sets of preferences and locations. | ||||||
|     """ |     """ | ||||||
|     response = invalid_client.post( |     response = invalid_client.post( | ||||||
|         "/trip/new", |         "/get/landmarks", | ||||||
|         json ={ |         json ={ | ||||||
|             "preferences": preferences, |             "preferences": preferences, | ||||||
|             "start": start |             "start": start | ||||||
|   | |||||||
| @@ -1,22 +1,32 @@ | |||||||
| """Collection of tests to ensure correct implementation and track progress of the get_landmarks_nearby feature. """ | """Collection of tests to ensure correct implementation and track progress of the get_landmarks_nearby feature. """ | ||||||
|  |  | ||||||
| from fastapi.testclient import TestClient | from fastapi.testclient import TestClient | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from ..main import app | from ..main import app | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="module") | @pytest.fixture(scope="module") | ||||||
| def client(): | def client(): | ||||||
|     """Client used to call the app.""" |     """Client used to call the app.""" | ||||||
|     return TestClient(app) |     return TestClient(app) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "location,status_code", |     "location,status_code", | ||||||
|     [ |     [ | ||||||
|         ([45.7576485, 4.8330241], 2000),     # Lyon, Bellecour. |         ([45.7576485, 4.8330241], 200),    # Lyon, France | ||||||
|         ([41.4020572, 2.1818985], 2000),       # Barcelona |         ([41.4020572, 2.1818985], 200),    # Barcelona, Spain | ||||||
|  |         ([59.3293, 18.0686], 200),         # Stockholm, Sweden | ||||||
|  |         ([43.6532, -79.3832], 200),        # Toronto, Canada | ||||||
|  |         ([38.7223, -9.1393], 200),         # Lisbon, Portugal | ||||||
|  |         ([6.5244, 3.3792], 200),           # Lagos, Nigeria | ||||||
|  |         ([17.3850, 78.4867], 200),         # Hyderabad, India | ||||||
|  |         ([30.0444, 31.2357], 200),         # Cairo, Egypt | ||||||
|  |         ([50.8503, 4.3517], 200),          # Brussels, Belgium | ||||||
|  |         ([35.2271, -80.8431], 200),        # Charlotte, USA | ||||||
|  |         ([10.4806, -66.9036], 200),        # Caracas, Venezuela | ||||||
|  |         ([9.51074, -13.71118], 200),       # Conakry, Guinea | ||||||
|     ] |     ] | ||||||
| ) | ) | ||||||
| def test_nearby(client, location, status_code):    # pylint: disable=redefined-outer-name | def test_nearby(client, location, status_code):    # pylint: disable=redefined-outer-name | ||||||
| @@ -27,9 +37,7 @@ def test_nearby(client, location, status_code):    # pylint: disable=redefined-o | |||||||
|         client: |         client: | ||||||
|         request: |         request: | ||||||
|     """ |     """ | ||||||
|     response = client.post(f"/landmarks/get-nearby/{location[0]}/{location[1]}") |     response = client.post(f"/get-nearby/landmarks/{location[0]}/{location[1]}") | ||||||
|     print(response) |  | ||||||
|     print(response.json()) |  | ||||||
|     suggestions = response.json() |     suggestions = response.json() | ||||||
|  |  | ||||||
|     # checks : |     # checks : | ||||||
|   | |||||||
| @@ -30,12 +30,13 @@ def test_invalid_input(client, location, radius, status_code):    # pylint: disa | |||||||
|         request: |         request: | ||||||
|     """ |     """ | ||||||
|     response = client.post( |     response = client.post( | ||||||
|         "/toilets/new", |         "/get/toilets", | ||||||
|         params={ |         params={ | ||||||
|             "location": location, |             "location": location, | ||||||
|             "radius": radius |             "radius": radius | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|  |     print(response.json()) | ||||||
|  |  | ||||||
|     # checks : |     # checks : | ||||||
|     assert response.status_code == status_code |     assert response.status_code == status_code | ||||||
| @@ -58,11 +59,12 @@ def test_no_toilets(client, location, status_code):    # pylint: disable=redefin | |||||||
|         request: |         request: | ||||||
|     """ |     """ | ||||||
|     response = client.post( |     response = client.post( | ||||||
|         "/toilets/new", |         "/get/toilets", | ||||||
|         params={ |         params={ | ||||||
|             "location": location |             "location": location | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|  |     print(response.json()) | ||||||
|     toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] |     toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] | ||||||
|  |  | ||||||
|     # checks : |     # checks : | ||||||
| @@ -87,12 +89,14 @@ def test_toilets(client, location, status_code):    # pylint: disable=redefined- | |||||||
|         request: |         request: | ||||||
|     """ |     """ | ||||||
|     response = client.post( |     response = client.post( | ||||||
|         "/toilets/new", |         "/get/toilets", | ||||||
|         params={ |         params={ | ||||||
|             "location": location, |             "location": location, | ||||||
|             "radius" : 600 |             "radius" : 600 | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     print(response.json()) | ||||||
|     toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] |     toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] | ||||||
|  |  | ||||||
|     # checks : |     # checks : | ||||||
|   | |||||||
| @@ -23,9 +23,9 @@ def client(): | |||||||
|         # Realistic |         # Realistic | ||||||
|         (5, 0, 0, 20, [48.0845881, 7.2804050], None),                   # Turckheim |         (5, 0, 0, 20, [48.0845881, 7.2804050], None),                   # Turckheim | ||||||
|         (5, 5, 5, 120, [45.7576485, 4.8330241], None),                  # Lyon, Bellecour |         (5, 5, 5, 120, [45.7576485, 4.8330241], None),                  # Lyon, Bellecour | ||||||
|         (5, 5, 5, 240, [50.9423526, 6.9577780], None),                  # Cologne, centre |         (5, 2, 5, 240, [50.9423526, 6.9577780], None),                  # Cologne, centre | ||||||
|         (5, 5, 5, 180, [48.5846589226, 7.74078715721], None),           # Strasbourg, centre |         (3, 5, 0, 180, [48.5846589226, 7.74078715721], None),           # Strasbourg, centre | ||||||
|         (5, 5, 5, 180, [47.377884227, 8.5395114066], None),             # Zurich, centre |         (2, 4, 5, 180, [47.377884227, 8.5395114066], None),             # Zurich, centre | ||||||
|         (5, 0, 5, 200, [48.85468881798671, 2.3423925755998374], None),  # Paris, centre |         (5, 0, 5, 200, [48.85468881798671, 2.3423925755998374], None),  # Paris, centre | ||||||
|         (5, 5, 5, 600, [40.72592726802, -73.9920434795], None),         # New York, Lower Manhattan |         (5, 5, 5, 600, [40.72592726802, -73.9920434795], None),         # New York, Lower Manhattan | ||||||
|     ] |     ] | ||||||
|   | |||||||
| @@ -1,16 +1,20 @@ | |||||||
| """Defines the endpoint for fetching toilet locations.""" | """API entry point for fetching toilet locations.""" | ||||||
|  | 
 | ||||||
| from fastapi import HTTPException, APIRouter, Query | from fastapi import HTTPException, APIRouter, Query | ||||||
| 
 | 
 | ||||||
| from ..structs.toilets import Toilets |  | ||||||
| from .toilets_manager import ToiletsManager | from .toilets_manager import ToiletsManager | ||||||
|  | from ..structs.toilets import Toilets | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Define the API router | # Initialize the API router | ||||||
| router = APIRouter() | router = APIRouter() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @router.post("/toilets/new") | @router.post("/get/toilets") | ||||||
| def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -> list[Toilets] : | 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. |     Endpoint to find toilets within a specified radius from a given location. | ||||||
| 
 | 
 | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| """Helper function to return only the major landmarks from a large list.""" |  | ||||||
| from ..structs.landmark import Landmark |  | ||||||
|  |  | ||||||
| def take_most_important( |  | ||||||
|     landmarks: list[Landmark], |  | ||||||
|     n_important: int |  | ||||||
| ) -> list[Landmark]: |  | ||||||
|     """ |  | ||||||
|     Given a list of landmarks, return the most important landmarks based on their attractiveness. |  | ||||||
|      |  | ||||||
|     Args: |  | ||||||
|         landmarks (list[Landmark]): List of landmarks that needs to be truncated |  | ||||||
|         n_important (int): Number of most important landmarks to return |  | ||||||
|      |  | ||||||
|     Returns: |  | ||||||
|         list[Landmark]: List of the n_important most important landmarks |  | ||||||
|     """ |  | ||||||
|     if n_important == 0 : |  | ||||||
|         raise ValueError('Number of landmarks to keep cannot be zero.') |  | ||||||
|  |  | ||||||
|     # Sort landmarks by attractiveness (descending) |  | ||||||
|     sorted_landmarks = sorted(landmarks, key=lambda x: x.attractiveness, reverse=True) |  | ||||||
|  |  | ||||||
|     return sorted_landmarks[:n_important] |  | ||||||
							
								
								
									
										1091
									
								
								report.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1091
									
								
								report.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user