diff --git a/camply/containers/goingtocamp.py b/camply/containers/goingtocamp.py new file mode 100644 index 00000000..12233245 --- /dev/null +++ b/camply/containers/goingtocamp.py @@ -0,0 +1,75 @@ +""" +GoingToCamp provider containers +""" +from typing import Any, Dict, List, Optional + +from camply.containers.base_container import CamplyModel + + +class ResourceLocation(CamplyModel): + """ + /api/maps + """ + + id: Optional[int] + rec_area_id: int + park_alerts: Optional[str] + resource_categories: Optional[List[int]] + resource_location_id: Optional[int] + resource_location_name: str + region_name: str + + +class ResourceAvailabilityUnit(CamplyModel): + """ + /api/availability/map: resourceAvailabilities + """ + + availability: int + remainingQuota: Optional[int] + + +class AvailabilityResponse(CamplyModel): + """ + /api/availability/map + """ + + mapId: int + mapAvailabilities: List[int] = [] + resourceAvailabilities: Dict[int, List[ResourceAvailabilityUnit]] = {} + mapLinkAvailabilities: Dict[str, Any] = {} + + +class ParamsBaseModel(CamplyModel): + """ + API and Booking URL Params + """ + + mapId: int + resourceLocationId: int + bookingCategoryId: int + startDate: str + endDate: str + isReserving: bool + partySize: int + + +class SearchFilter(ParamsBaseModel): + """ + /api/availability/map: API Filter + """ + + equipmentCategoryId: Optional[int] = None + filterData: List[Any] = [] + subEquipmentCategoryId: Optional[int] = None + numEquipment: int + getDailyAvailability: bool + + +class BookingUrlParams(ParamsBaseModel): + """ + Booking URL Params + """ + + equipmentId: Optional[int] = None + subEquipmentId: Optional[int] = None diff --git a/camply/containers/gtc_api_responses.py b/camply/containers/gtc_api_responses.py deleted file mode 100644 index 0ba0d33f..00000000 --- a/camply/containers/gtc_api_responses.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -GoingToCamp provider containers -""" -from typing import List, Optional - -from camply.containers.base_container import CamplyModel - - -class ResourceLocation(CamplyModel): - """ - /api/maps - """ - - id: Optional[int] - rec_area_id: int - park_alerts: Optional[str] - resource_categories: Optional[List[int]] - resource_location_id: Optional[int] - resource_location_name: str - region_name: str diff --git a/camply/providers/going_to_camp/going_to_camp_provider.py b/camply/providers/going_to_camp/going_to_camp_provider.py index c395595e..7222b5ff 100644 --- a/camply/providers/going_to_camp/going_to_camp_provider.py +++ b/camply/providers/going_to_camp/going_to_camp_provider.py @@ -7,6 +7,7 @@ import sys from datetime import datetime from typing import Any, Dict, List, Optional, Tuple, Union +from urllib.parse import urlencode import requests from fake_useragent import UserAgent @@ -14,7 +15,13 @@ from camply.containers import AvailableResource, CampgroundFacility, RecreationArea from camply.containers.base_container import GoingToCampEquipment -from camply.containers.gtc_api_responses import ResourceLocation +from camply.containers.goingtocamp import ( + AvailabilityResponse, + BookingUrlParams, + ResourceAvailabilityUnit, + ResourceLocation, + SearchFilter, +) from camply.providers.base_provider import BaseProvider, ProviderSearchError from camply.providers.going_to_camp.rec_areas import RECREATION_AREAS from camply.utils import make_list @@ -41,6 +48,25 @@ } +class AvailabilityStatuses: + """ + Availability Statuses + + These represent the values from the GoingToCamp + "Availability Legend" + """ + + AVAILABLE = 0 + UNAVAILABLE = 1 + NOT_OPERATING = 2 + NON_RESERVABLE = 3 + CLOSED = 4 + INVALID = 5 + INVALID_BOOKING_CATEGORY = 6 + PARTIALLY_AVAILABLE = 7 + HELD = 8 + + class GoingToCamp(BaseProvider): """ Going To Camp API provider @@ -216,28 +242,26 @@ def get_reservation_link( """ if not sub_equipment_id: sub_equipment_id = "" - - return ( - "https://%s/create-booking/results?mapId=%s" - "&bookingCategoryId=0" - "&startDate=%s" - "&endDate=%s" - "&isReserving=true" - "&equipmentId=%s" - "&subEquipmentId=%s" - "&partySize=%s" - "&resourceLocationId=%s" - % ( - rec_area_domain_name, - map_id, - start_date.isoformat(), - end_date.isoformat(), - equipment_id, - sub_equipment_id, - party_size, - resource_location_id, - ) + url = f"https://{rec_area_domain_name}/create-booking/results" + if sub_equipment_id in (None, ""): + sub_equipment_id = NON_GROUP_EQUIPMENT + query_params = BookingUrlParams( + mapId=map_id, + bookingCategoryId=0, + startDate=start_date.isoformat(), + endDate=end_date.isoformat(), + isReserving=True, + equipmentId=equipment_id, + subEquipmentId=sub_equipment_id, + partySize=party_size, + resourceLocationId=resource_location_id, + ) + booking_url = ( + url + + "?" + + urlencode(query_params.dict(exclude_unset=True, exclude_none=True)) ) + return booking_url def find_facilities_per_recreation_area( self, @@ -326,7 +350,7 @@ def _api_request( rec_area_id: int, endpoint_name: str, params: Optional[Dict[str, str]] = None, - ) -> str: + ) -> Dict[str, Any]: if params is None: params = {} @@ -442,13 +466,19 @@ def _process_facilities_responses( ) return facility, campground_facility - def _find_matching_resources(self, rec_area_id: int, search_filter: Dict[str, any]): - results = self._api_request(rec_area_id, "MAPDATA", search_filter) + def _find_matching_resources( + self, rec_area_id: int, search_filter: SearchFilter + ) -> Tuple[Dict[int, Dict[int, List[ResourceAvailabilityUnit]]], List[str]]: + results = self._api_request( + rec_area_id, + "MAPDATA", + search_filter.dict(exclude_unset=True, exclude_none=True), + ) + result_parsed = AvailabilityResponse(**results) availability_details = { - search_filter["mapId"]: results["resourceAvailabilities"] + result_parsed.mapId: result_parsed.resourceAvailabilities } - - return availability_details, list(results["mapLinkAvailabilities"].keys()) + return availability_details, list(result_parsed.mapLinkAvailabilities.keys()) def list_equipment_types(self, rec_area_id: int) -> Dict[str, int]: """ @@ -501,41 +531,38 @@ def list_site_availability( available_sites: List[AvailableResource] The list of available sites """ - search_filter = { - "mapId": campground.map_id, - "resourceLocationId": campground.facility_id, - "bookingCategoryId": 0, - "startDate": start_date.isoformat(), - "endDate": end_date.isoformat(), - "isReserving": True, - "getDailyAvailability": False, - "partySize": 1, - "numEquipment": 1, - "equipmentCategoryId": NON_GROUP_EQUIPMENT, - "filterData": [], - } + search_filter = SearchFilter( + mapId=campground.map_id, + resourceLocationId=campground.facility_id, + bookingCategoryId=0, + startDate=start_date.isoformat(), + endDate=end_date.isoformat(), + isReserving=True, + getDailyAvailability=False, + partySize=1, + numEquipment=1, + filterData=[], + ) if equipment_type_id: - search_filter["subEquipmentCategoryId"] = equipment_type_id - + search_filter.equipmentCategoryId = NON_GROUP_EQUIPMENT + search_filter.subEquipmentCategoryId = equipment_type_id resources, additional_resources = self._find_matching_resources( - campground.recreation_area_id, search_filter + rec_area_id=campground.recreation_area_id, search_filter=search_filter ) - # Resources are often deeply nested; fetch nested resources for map_id in additional_resources: - search_filter["mapId"] = map_id + search_filter.mapId = map_id avail, _ = self._find_matching_resources( - campground.recreation_area_id, search_filter + rec_area_id=campground.recreation_area_id, search_filter=search_filter ) resources.update(avail) - availabilities = [] for map_id, resource_details in resources.items(): for resource_id, availability_details in resource_details.items(): - if availability_details[0]["availability"] == 0: + availability_enum = availability_details[0].availability + if availability_enum == AvailabilityStatuses.AVAILABLE: ar = AvailableResource(resource_id=resource_id, map_id=map_id) availabilities.append(ar) - return availabilities diff --git a/camply/search/search_going_to_camp.py b/camply/search/search_going_to_camp.py index cb732fdf..8cdc0a68 100644 --- a/camply/search/search_going_to_camp.py +++ b/camply/search/search_going_to_camp.py @@ -175,7 +175,7 @@ def get_all_campsites(self) -> List[AvailableCampsite]: # be viable for camping. Skip all zero-capacity sites. if ( not site_details["minCapacity"] - or not site_details["maxCapacity"] + and not site_details["maxCapacity"] ): continue @@ -193,7 +193,7 @@ def get_all_campsites(self) -> List[AvailableCampsite]: "Service Type", "Unknown" ), campsite_occupancy=( - site_details["minCapacity"], + site_details["minCapacity"] or 0, site_details["maxCapacity"], ), campsite_use_type="N/A", diff --git a/tests/cli/cassettes/test_goingtocamp_equipment_types.yaml b/tests/cli/cassettes/test_goingtocamp_equipment_types.yaml index c85b2857..f3ceffc1 100644 --- a/tests/cli/cassettes/test_goingtocamp_equipment_types.yaml +++ b/tests/cli/cassettes/test_goingtocamp_equipment_types.yaml @@ -9,8 +9,8 @@ interactions: Connection: - keep-alive User-Agent: - - Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) - Chrome/3.0.197.0 Safari/532.0 + - Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.17 + (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17 method: GET uri: https://longpoint.goingtocamp.com/api/equipment response: @@ -32,7 +32,7 @@ interactions: Content-Type: - application/json; charset=utf-8 Date: - - Sat, 22 Apr 2023 17:58:30 GMT + - Thu, 31 Aug 2023 15:36:35 GMT Pragma: - no-cache Referrer-Policy: @@ -40,9 +40,9 @@ interactions: Request-Context: - appId=cid-v1:03d8b028-e287-44e2-a3af-4195efd11ce4 Set-Cookie: - - .AspNetCore.Antiforgery.3YREhQdkuHQ=CfDJ8CnXzG7UPapGgSMv5mpOJFYMHpAgEmuZULKN88thRalPTR28P_1djG1MiQ5365yZUhb6JVFCIVBGhbPAcgmZuoBeVILcvE7GHRk16hj9-6bgdfP1XgmP-0xr4votZWnh9KeVsz_y3E0ZHbkbBGE5wkE; + - .AspNetCore.Antiforgery.3YREhQdkuHQ=CfDJ8Kcxd2mnrChGmVWDGR44_Q54ickGr4Xe-u8lNRx71KaQSDPgqBr2Pcxpcpt-fVwDV15T0xq3yl27OaaV0UL9ApwNA58WvRaTRGHJBgk8xWWUy5VxZnmH9WjhYZAOA4d35x_XXjF7XRJ8fW7SxXAtEP4; path=/; samesite=strict; httponly - - XSRF-TOKEN=CfDJ8CnXzG7UPapGgSMv5mpOJFbV-Q1ho-6sINEDsGACo53Y_baPUErn7Y0nIerq41UWQsM8kybc9ZklvbLLQzZfCPeKjoKX_R9PDuMUFsJXNmPaTi_BCWcjo0-AA9ZdhQDd9IcQEE3OUUqORU_7b9a1G_Q; + - XSRF-TOKEN=CfDJ8Kcxd2mnrChGmVWDGR44_Q5NapQOxRMpm5jGaZTSxBMLLBO3dp5QlotvsvVaPjVbvmQ9epCryXA9R9qp6tBsYnnULWzTQB7w_C6ROFBIgCxHxPQ6eG-GQbaOcdAdOASzq_V5pyD8hpVfpjFLeAaLB40; path=/; secure Strict-Transport-Security: - max-age=31536000 @@ -61,7 +61,7 @@ interactions: content-length: - "1266" x-azure-ref: - - 20230422T175830Z-whu0m374n55zbbqt6rxaenaw1s00000000v0000000009vud + - 20230831T153635Z-ttsvts4b5t27d9wfenxn58c1d800000001kg0000000047hm status: code: 200 message: OK diff --git a/tests/search_providers/test_goingtocamp_search.py b/tests/search_providers/test_goingtocamp_search.py index 234df8f5..21bdeb3f 100644 --- a/tests/search_providers/test_goingtocamp_search.py +++ b/tests/search_providers/test_goingtocamp_search.py @@ -24,8 +24,8 @@ def search_window() -> SearchWindow: SearchWindow """ search_window = SearchWindow( - start_date=datetime(2023, 9, 1), - end_date=datetime(2023, 9, 2), + start_date=datetime(2023, 9, 15), + end_date=datetime(2023, 9, 16), ) return search_window @@ -41,8 +41,8 @@ def going_to_camp_finder(search_window) -> SearchGoingToCamp: """ gtc_finder = SearchGoingToCamp( search_window=search_window, - recreation_area=[1], # Long Point Region, Ontario - campgrounds="-2147483643", # Waterford North Conservation Area + recreation_area=[14], # Parks Canada + campgrounds="-2147483617", # Fundy - Chignecto ) logger.info("GoingToCamp Campsite Searcher Established.") logger.info(f"Search Months: {gtc_finder.search_months}")