From 07dde5ab58f8408fc1bad24bb67c79ecbe31cc5d Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Wed, 31 Jul 2024 12:54:25 +0200 Subject: [PATCH] persistence for recurring api calls --- backend/Dockerfile | 1 + backend/Pipfile | 1 + backend/Pipfile.lock | 25 +++++++++++----- backend/src/constants.py | 4 +++ backend/src/main.py | 36 ++++++++++++++-------- backend/src/persistence.py | 18 +++++++++++ backend/src/structs/linked_landmarks.py | 22 ++------------ backend/src/structs/preferences.py | 3 +- backend/src/structs/trip.py | 28 +++++++++++++++++ backend/src/tester.py | 21 ++++--------- backend/src/utils/landmarks_manager.py | 40 ++++++++++--------------- 11 files changed, 118 insertions(+), 81 deletions(-) create mode 100644 backend/src/persistence.py create mode 100644 backend/src/structs/trip.py diff --git a/backend/Dockerfile b/backend/Dockerfile index e399a4f..4a3ca4d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -13,5 +13,6 @@ EXPOSE 8000 # Set environment variables used by the deployment. These can be overridden by the user using this image. ENV NUM_WORKERS=1 ENV OSM_CACHE_DIR=/cache +ENV MEMCACHED_HOST=none CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS diff --git a/backend/Pipfile b/backend/Pipfile index c590ded..1ca056b 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -14,3 +14,4 @@ shapely = "*" scipy = "*" osmpythontools = "*" pywikibot = "*" +pymemcache = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 387fcbd..a6b756c 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f0de801038593d42d8b780d14c2c72bb4f5f5e66df02f72244917ede5d5ebce6" + "sha256": "4f8b3f0395b4e5352330616870da13acf41e16d1b69ba31b15fd688e90b8b628" }, "pipfile-spec": 6, "requires": {}, @@ -1102,6 +1102,15 @@ "markers": "python_version >= '3.8'", "version": "==2.18.0" }, + "pymemcache": { + "hashes": [ + "sha256:27bf9bd1bbc1e20f83633208620d56de50f14185055e49504f4f5e94e94aff94", + "sha256:f507bc20e0dc8d562f8df9d872107a278df049fa496805c1431b926f3ddd0eab" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.0.0" + }, "pyparsing": { "hashes": [ "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", @@ -1142,12 +1151,12 @@ }, "pywikibot": { "hashes": [ - "sha256:3f4fbc57f1765aa0fa1ccf84125bcfa475cae95b9cc0291867b751f3d4ac8fa2", - "sha256:a26d918cf88ef56fdb1421b65b09def200cc28031cdc922d72a4198fbfddd225" + "sha256:0dd8291f1a26abb9fce2c2108a90dc338274988e60d21723aec1d3b0de321b5e", + "sha256:7953fc4a6c498057e6eb7d9b762bbccb61348af0a599b89d7e246d5175b20a9b" ], "index": "pypi", "markers": "python_full_version >= '3.7.0'", - "version": "==9.2.1" + "version": "==9.3.0" }, "pyyaml": { "hashes": [ @@ -1349,7 +1358,7 @@ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.13'", "version": "==4.12.2" }, "tzdata": { @@ -1658,11 +1667,11 @@ }, "xarray": { "hashes": [ - "sha256:0b91e0bc4dc0296947947640fe31ec6e867ce258d2f7cbc10bedf4a6d68340c7", - "sha256:721a7394e8ec3d592b2d8ebe21eed074ac077dc1bb1bd777ce00e41700b4866c" + "sha256:1b0fd51ec408474aa1f4a355d75c00cc1c02bd425d97b2c2e551fd21810e7f64", + "sha256:4cae512d121a8522d41e66d942fb06c526bc1fd32c2c181d5fe62fe65b671638" ], "markers": "python_version >= '3.9'", - "version": "==2024.6.0" + "version": "==2024.7.0" } }, "develop": {} diff --git a/backend/src/constants.py b/backend/src/constants.py index 1b57a3a..7118666 100644 --- a/backend/src/constants.py +++ b/backend/src/constants.py @@ -25,3 +25,7 @@ logging.config.dictConfig(config) # if we are in a debug session, set the log level to debug if os.getenv('DEBUG', False): logging.getLogger().setLevel(logging.DEBUG) + +MEMCACHE_HOST = os.getenv('MEMCACHE_HOST', None) +if MEMCACHE_HOST == "none": + MEMCACHE_HOST = None diff --git a/backend/src/main.py b/backend/src/main.py index cb245cb..c59d864 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,12 +1,14 @@ import logging -from fastapi import FastAPI, Query, Body +from fastapi import FastAPI, Query, Body, HTTPException from structs.landmark import Landmark from structs.preferences import Preferences from structs.linked_landmarks import LinkedLandmarks +from structs.trip import Trip from utils.landmarks_manager import LandmarkManager from utils.optimizer import Optimizer from utils.refiner import Refiner +from persistence import client as cache_client logger = logging.getLogger(__name__) @@ -17,8 +19,8 @@ optimizer = Optimizer() refiner = Refiner(optimizer=optimizer) -@app.post("/route/new") -def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> str: +@app.post("/trip/new") +def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[float, float] | None = None) -> Trip: ''' Main function to call the optimizer. :param preferences: the preferences specified by the user as the post body @@ -47,22 +49,32 @@ def get_route(preferences: Preferences, start: tuple[float, float], end: tuple[f landmarks_short.insert(0, start_landmark) landmarks_short.append(end_landmark) - # TODO infer these parameters from the preferences - max_walking_time = 4 # hours - detour = 30 # minutes - # First stage optimization - base_tour = optimizer.solve_optimization(max_walking_time*60, landmarks_short) + base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) # Second stage optimization - refined_tour = refiner.refine_optimization(landmarks, base_tour, max_walking_time*60, detour) + refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute) linked_tour = LinkedLandmarks(refined_tour) - return linked_tour[0].uuid + # upon creation of the trip, persistence of both the trip and its landmarks is ensured. Ca + trip = Trip.from_linked_landmarks(linked_tour, cache_client) + return trip +#### For already existing trips/landmarks +@app.get("/trip/{trip_uuid}") +def get_trip(trip_uuid: str) -> Trip: + try: + trip = cache_client.get(f"trip_{trip_uuid}") + return trip + except KeyError: + raise HTTPException(status_code=404, detail="Trip not found") + @app.get("/landmark/{landmark_uuid}") def get_landmark(landmark_uuid: str) -> Landmark: - #cherche dans linked_tour et retourne le landmark correspondant - pass + try: + landmark = cache_client.get(f"landmark_{landmark_uuid}") + return landmark + except KeyError: + raise HTTPException(status_code=404, detail="Landmark not found") \ No newline at end of file diff --git a/backend/src/persistence.py b/backend/src/persistence.py new file mode 100644 index 0000000..249edc6 --- /dev/null +++ b/backend/src/persistence.py @@ -0,0 +1,18 @@ +from pymemcache.client.base import Client + +import constants + + +class DummyClient: + _data = {} + def set(self, key, value, **kwargs): + self._data[key] = value + + def get(self, key, **kwargs): + return self._data[key] + + +if constants.MEMCACHE_HOST is None: + client = DummyClient() +else: + client = Client(constants.MEMCACHE_HOST, timeout=1) diff --git a/backend/src/structs/linked_landmarks.py b/backend/src/structs/linked_landmarks.py index a0799f3..7d350f5 100644 --- a/backend/src/structs/linked_landmarks.py +++ b/backend/src/structs/linked_landmarks.py @@ -1,4 +1,3 @@ -import uuid from .landmark import Landmark from utils.get_time_separation import get_time @@ -9,8 +8,7 @@ class LinkedLandmarks: """ _landmarks = list[Landmark] - total_time = int - uuid = str + total_time: int = 0 def __init__(self, data: list[Landmark] = None) -> None: """ @@ -19,7 +17,6 @@ class LinkedLandmarks: Args: data (list[Landmark], optional): The list of landmarks that are linked together. Defaults to None. """ - self.uuid = uuid.uuid4() self._landmarks = data if data else [] self._link_landmarks() @@ -28,7 +25,6 @@ class LinkedLandmarks: """ Create the links between the landmarks in the list by setting their .next_uuid and the .time_to_next attributes. """ - self.total_time = 0 for i, landmark in enumerate(self._landmarks[:-1]): landmark.next_uuid = self._landmarks[i + 1].uuid time_to_next = get_time(landmark.location, self._landmarks[i + 1].location) @@ -44,18 +40,4 @@ class LinkedLandmarks: def __str__(self) -> str: - return f"LinkedLandmarks, total time: {self.total_time} minutes, {len(self._landmarks)} stops: [{','.join([str(landmark) for landmark in self._landmarks])}]" - - - def asdict(self) -> dict: - """ - Convert the linked landmarks to a json serializable dictionary. - - Returns: - dict: A dictionary representation of the linked landmarks. - """ - return { - 'uuid': self.uuid, - 'total_time': self.total_time, - 'landmarks': [landmark.dict() for landmark in self._landmarks] - } + return f"LinkedLandmarks [{' ->'.join([str(landmark) for landmark in self._landmarks])}]" diff --git a/backend/src/structs/preferences.py b/backend/src/structs/preferences.py index d370fc8..ebb15b7 100644 --- a/backend/src/structs/preferences.py +++ b/backend/src/structs/preferences.py @@ -2,7 +2,6 @@ from pydantic import BaseModel from typing import Optional, Literal class Preference(BaseModel) : - name: str type: Literal['sightseeing', 'nature', 'shopping', 'start', 'finish'] score: int # score could be from 1 to 5 @@ -17,5 +16,5 @@ class Preferences(BaseModel) : # Shopping (diriger plutôt vers des zones / rues commerçantes) shopping : Preference - max_time_minute: Optional[int] = 6*60 + max_time_minute: Optional[int] = 6*60 detour_tolerance_minute: Optional[int] = 0 diff --git a/backend/src/structs/trip.py b/backend/src/structs/trip.py new file mode 100644 index 0000000..c2435a3 --- /dev/null +++ b/backend/src/structs/trip.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field + +from .landmark import Landmark +from .linked_landmarks import LinkedLandmarks +import uuid + +class Trip(BaseModel): + uuid: str = Field(default_factory=uuid.uuid4) + total_time: int + first_landmark_uuid: str + + + @classmethod + def from_linked_landmarks(self, landmarks: LinkedLandmarks, cache_client) -> "Trip": + """ + Initialize a new Trip object and ensure it is stored in the cache. + """ + trip = Trip( + total_time = landmarks.total_time, + first_landmark_uuid = str(landmarks[0].uuid) + ) + + # Store the trip in the cache + cache_client.set(f"trip_{trip.uuid}", trip) + for landmark in landmarks: + cache_client.set(f"landmark_{landmark.uuid}", landmark) + + return trip \ No newline at end of file diff --git a/backend/src/tester.py b/backend/src/tester.py index 8a39967..f83c4a7 100644 --- a/backend/src/tester.py +++ b/backend/src/tester.py @@ -20,22 +20,13 @@ def test(start_coords: tuple[float, float], finish_coords: tuple[float, float] = preferences = Preferences( - sightseeing=Preference( - name='sightseeing', - type='sightseeing', - score = 5), - nature=Preference( - name='nature', - type='nature', - score = 5), - shopping=Preference( - name='shopping', - type='shopping', - score = 5), + sightseeing=Preference(type='sightseeing', score = 5), + nature=Preference(type='nature', score = 5), + shopping=Preference(type='shopping', score = 5), - max_time_minute=180, - detour_tolerance_minute=30 - ) + max_time_minute=180, + detour_tolerance_minute=30 + ) # Create start and finish if finish_coords is None : diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index 2ba9100..2422c11 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -15,10 +15,6 @@ from .take_most_important import take_most_important import constants -SIGHTSEEING = 'sightseeing' -NATURE = 'nature' -SHOPPING = 'shopping' - class LandmarkManager: @@ -74,25 +70,25 @@ class LandmarkManager: # list for sightseeing if preferences.sightseeing.score != 0: score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff) - L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], SIGHTSEEING, score_function) - self.correct_score(L1, preferences.sightseeing) + L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function) L += L1 # list for nature if preferences.nature.score != 0: score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff) - L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], NATURE, score_function) - self.correct_score(L2, preferences.nature) + L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function) L += L2 # list for shopping if preferences.shopping.score != 0: score_function = lambda loc, n_tags: int(self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff)) - L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], SHOPPING, score_function) - self.correct_score(L3, preferences.shopping) + L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) L += L3 + L = self.remove_duplicates(L) + self.correct_score(L, preferences) + L_constrained = take_most_important(L, self.N_important) self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.') @@ -123,7 +119,7 @@ class LandmarkManager: return L_clean - def correct_score(self, landmarks: list[Landmark], preference: Preference): + def correct_score(self, landmarks: list[Landmark], preferences: Preferences) -> None: """ Adjust the attractiveness score of each landmark in the list based on user preferences. @@ -132,20 +128,16 @@ class LandmarkManager: Args: landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected. - preference (Preference): The user's preference settings that influence the attractiveness score adjustment. - - Raises: - TypeError: If the type of any landmark in the list does not match the expected type in the preference. + preferences (Preferences): The user's preference settings that influence the attractiveness score adjustment. """ - if len(landmarks) == 0: - return - - if landmarks[0].type != preference.type: - raise TypeError(f"LandmarkType {preference.type} does not match the type of Landmark {landmarks[0].name}") - - for elem in landmarks: - elem.attractiveness = int(elem.attractiveness*preference.score/5) # arbitrary computation + score_dict = { + preferences.sightseeing.type: preferences.sightseeing.score, + preferences.nature.type: preferences.nature.score, + preferences.shopping.type: preferences.shopping.score + } + for landmark in landmarks: + landmark.attractiveness = int(landmark.attractiveness * score_dict[landmark.type] / 5) def count_elements_close_to(self, coordinates: tuple[float, float]) -> int: @@ -310,7 +302,7 @@ class LandmarkManager: if "leisure" in tag and elem.tag('leisure') == "park": elem_type = "nature" - if landmarktype != SHOPPING: + if landmarktype != "shopping": if "shop" in tag: skip = True break