From ce45b9c98df5cb2fabd7891ddcf754656abffe28 Mon Sep 17 00:00:00 2001 From: juftin Date: Wed, 23 Aug 2023 20:54:49 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20goingtocamp=20missing=20site?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- camply/containers/gtc_api_responses.py | 41 +++++++++++- .../going_to_camp/going_to_camp_provider.py | 63 ++++++++++--------- camply/search/search_going_to_camp.py | 4 +- .../test_goingtocamp_equipment_types.yaml | 12 ++-- .../test_goingtocamp_search.py | 4 +- 5 files changed, 84 insertions(+), 40 deletions(-) diff --git a/camply/containers/gtc_api_responses.py b/camply/containers/gtc_api_responses.py index 0ba0d33f..775d9c8c 100644 --- a/camply/containers/gtc_api_responses.py +++ b/camply/containers/gtc_api_responses.py @@ -1,7 +1,7 @@ """ GoingToCamp provider containers """ -from typing import List, Optional +from typing import Any, Dict, List, Optional from camply.containers.base_container import CamplyModel @@ -18,3 +18,42 @@ class ResourceLocation(CamplyModel): 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 SearchFilter(CamplyModel): + """ + /api/availability/map: API Filter + """ + + mapId: int + resourceLocationId: int + bookingCategoryId: int + startDate: str + endDate: str + isReserving: bool + getDailyAvailability: bool + partySize: int + numEquipment: int + equipmentCategoryId: int + filterData: List[Any] = [] + subEquipmentCategoryId: Optional[int] = None 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..9251bd74 100644 --- a/camply/providers/going_to_camp/going_to_camp_provider.py +++ b/camply/providers/going_to_camp/going_to_camp_provider.py @@ -14,7 +14,12 @@ 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.gtc_api_responses import ( + AvailabilityResponse, + 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 @@ -326,7 +331,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 +447,17 @@ 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) + ) + 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 +510,37 @@ 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, + equipmentCategoryId=NON_GROUP_EQUIPMENT, + filterData=[], + ) if equipment_type_id: - search_filter["subEquipmentCategoryId"] = equipment_type_id - + 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: + if availability_details[0].availability in [0, 5]: 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..96ed1ded 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 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, + like Gecko) Chrome/4.0.212.0 Safari/532.0 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, 24 Aug 2023 02:02:36 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_Q5lNBMjn83HKnTfOsXb5WuBppnPRSSWzAOqtlroLxlyXT0KBToVQvRlX9iwo3r2d3jiAx6X_FbtKmBhGrgqybwv8oDKXKyTrx5VnHpv5crY71E6tsrK2uEvNNSZ7xqKkBk; path=/; samesite=strict; httponly - - XSRF-TOKEN=CfDJ8CnXzG7UPapGgSMv5mpOJFbV-Q1ho-6sINEDsGACo53Y_baPUErn7Y0nIerq41UWQsM8kybc9ZklvbLLQzZfCPeKjoKX_R9PDuMUFsJXNmPaTi_BCWcjo0-AA9ZdhQDd9IcQEE3OUUqORU_7b9a1G_Q; + - XSRF-TOKEN=CfDJ8Kcxd2mnrChGmVWDGR44_Q6TIHE-G76soKT_7ZrHperZhe9abn0K1K_Gg6_B-YswH4q-nOICTIKA-rgdyCw_oetoVGAXGaYA8s0IS9lBXo2VZcGLWqn8yP48m4PkdCzMG5_3_7m2rpmqQ42ZEWV4vQc; path=/; secure Strict-Transport-Security: - max-age=31536000 @@ -61,7 +61,7 @@ interactions: content-length: - "1266" x-azure-ref: - - 20230422T175830Z-whu0m374n55zbbqt6rxaenaw1s00000000v0000000009vud + - 20230824T020236Z-q15212mfxx66dcpxhvrn2zbt9g000000021g00000000rr1g 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..9245d63e 100644 --- a/tests/search_providers/test_goingtocamp_search.py +++ b/tests/search_providers/test_goingtocamp_search.py @@ -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}") From 2f743e1f5e64da973c9b75bc8988093e510b035d Mon Sep 17 00:00:00 2001 From: juftin Date: Wed, 30 Aug 2023 23:11:12 -0600 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9B=20goingtocamp=20availability?= =?UTF-8?q?=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../going_to_camp/going_to_camp_provider.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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 9251bd74..af6aab7a 100644 --- a/camply/providers/going_to_camp/going_to_camp_provider.py +++ b/camply/providers/going_to_camp/going_to_camp_provider.py @@ -46,6 +46,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 @@ -538,7 +557,14 @@ def list_site_availability( availabilities = [] for map_id, resource_details in resources.items(): for resource_id, availability_details in resource_details.items(): - if availability_details[0].availability in [0, 5]: + availability_enum = availability_details[0].availability + if any( + [ + availability_enum == AvailabilityStatuses.AVAILABLE, + availability_enum == AvailabilityStatuses.INVALID + and equipment_type_id is None, + ] + ): ar = AvailableResource(resource_id=resource_id, map_id=map_id) availabilities.append(ar) return availabilities From f7e1e88f13f20ec62d837c9d59591a0ea0a4e84f Mon Sep 17 00:00:00 2001 From: juftin Date: Fri, 1 Sep 2023 16:53:03 -0600 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B=20goingtocamp=20equipment=20fi?= =?UTF-8?q?lter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- camply/containers/gtc_api_responses.py | 2 +- .../going_to_camp/going_to_camp_provider.py | 51 ++++++++----------- .../test_goingtocamp_equipment_types.yaml | 12 ++--- .../test_goingtocamp_search.py | 4 +- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/camply/containers/gtc_api_responses.py b/camply/containers/gtc_api_responses.py index 775d9c8c..e03649b9 100644 --- a/camply/containers/gtc_api_responses.py +++ b/camply/containers/gtc_api_responses.py @@ -54,6 +54,6 @@ class SearchFilter(CamplyModel): getDailyAvailability: bool partySize: int numEquipment: int - equipmentCategoryId: int + equipmentCategoryId: Optional[int] = None filterData: List[Any] = [] subEquipmentCategoryId: Optional[int] = None 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 af6aab7a..155e690f 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 @@ -240,28 +241,20 @@ 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" + query_params = { + "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) + return booking_url def find_facilities_per_recreation_area( self, @@ -470,7 +463,9 @@ 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) + rec_area_id, + "MAPDATA", + search_filter.dict(exclude_unset=True, exclude_none=True), ) result_parsed = AvailabilityResponse(**results) availability_details = { @@ -539,10 +534,10 @@ def list_site_availability( getDailyAvailability=False, partySize=1, numEquipment=1, - equipmentCategoryId=NON_GROUP_EQUIPMENT, filterData=[], ) if equipment_type_id: + search_filter.equipmentCategoryId = NON_GROUP_EQUIPMENT search_filter.subEquipmentCategoryId = equipment_type_id resources, additional_resources = self._find_matching_resources( rec_area_id=campground.recreation_area_id, search_filter=search_filter @@ -558,13 +553,7 @@ def list_site_availability( for map_id, resource_details in resources.items(): for resource_id, availability_details in resource_details.items(): availability_enum = availability_details[0].availability - if any( - [ - availability_enum == AvailabilityStatuses.AVAILABLE, - availability_enum == AvailabilityStatuses.INVALID - and equipment_type_id is None, - ] - ): + if availability_enum == AvailabilityStatuses.AVAILABLE: ar = AvailableResource(resource_id=resource_id, map_id=map_id) availabilities.append(ar) return availabilities diff --git a/tests/cli/cassettes/test_goingtocamp_equipment_types.yaml b/tests/cli/cassettes/test_goingtocamp_equipment_types.yaml index 96ed1ded..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 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, - like Gecko) Chrome/4.0.212.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: - - Thu, 24 Aug 2023 02:02:36 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=CfDJ8Kcxd2mnrChGmVWDGR44_Q5lNBMjn83HKnTfOsXb5WuBppnPRSSWzAOqtlroLxlyXT0KBToVQvRlX9iwo3r2d3jiAx6X_FbtKmBhGrgqybwv8oDKXKyTrx5VnHpv5crY71E6tsrK2uEvNNSZ7xqKkBk; + - .AspNetCore.Antiforgery.3YREhQdkuHQ=CfDJ8Kcxd2mnrChGmVWDGR44_Q54ickGr4Xe-u8lNRx71KaQSDPgqBr2Pcxpcpt-fVwDV15T0xq3yl27OaaV0UL9ApwNA58WvRaTRGHJBgk8xWWUy5VxZnmH9WjhYZAOA4d35x_XXjF7XRJ8fW7SxXAtEP4; path=/; samesite=strict; httponly - - XSRF-TOKEN=CfDJ8Kcxd2mnrChGmVWDGR44_Q6TIHE-G76soKT_7ZrHperZhe9abn0K1K_Gg6_B-YswH4q-nOICTIKA-rgdyCw_oetoVGAXGaYA8s0IS9lBXo2VZcGLWqn8yP48m4PkdCzMG5_3_7m2rpmqQ42ZEWV4vQc; + - 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: - - 20230824T020236Z-q15212mfxx66dcpxhvrn2zbt9g000000021g00000000rr1g + - 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 9245d63e..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 From 270d05195b851d4eb5e42f62838083e3a429f2f4 Mon Sep 17 00:00:00 2001 From: juftin Date: Fri, 1 Sep 2023 17:30:00 -0600 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20goingtocamp=20keep=20booking?= =?UTF-8?q?=20url=20the=20same?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{gtc_api_responses.py => goingtocamp.py} | 24 +++++++++++--- .../going_to_camp/going_to_camp_provider.py | 33 +++++++++++-------- 2 files changed, 40 insertions(+), 17 deletions(-) rename camply/containers/{gtc_api_responses.py => goingtocamp.py} (81%) diff --git a/camply/containers/gtc_api_responses.py b/camply/containers/goingtocamp.py similarity index 81% rename from camply/containers/gtc_api_responses.py rename to camply/containers/goingtocamp.py index e03649b9..12233245 100644 --- a/camply/containers/gtc_api_responses.py +++ b/camply/containers/goingtocamp.py @@ -40,9 +40,9 @@ class AvailabilityResponse(CamplyModel): mapLinkAvailabilities: Dict[str, Any] = {} -class SearchFilter(CamplyModel): +class ParamsBaseModel(CamplyModel): """ - /api/availability/map: API Filter + API and Booking URL Params """ mapId: int @@ -51,9 +51,25 @@ class SearchFilter(CamplyModel): startDate: str endDate: str isReserving: bool - getDailyAvailability: bool partySize: int - numEquipment: 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/providers/going_to_camp/going_to_camp_provider.py b/camply/providers/going_to_camp/going_to_camp_provider.py index 155e690f..7222b5ff 100644 --- a/camply/providers/going_to_camp/going_to_camp_provider.py +++ b/camply/providers/going_to_camp/going_to_camp_provider.py @@ -15,8 +15,9 @@ from camply.containers import AvailableResource, CampgroundFacility, RecreationArea from camply.containers.base_container import GoingToCampEquipment -from camply.containers.gtc_api_responses import ( +from camply.containers.goingtocamp import ( AvailabilityResponse, + BookingUrlParams, ResourceAvailabilityUnit, ResourceLocation, SearchFilter, @@ -242,18 +243,24 @@ def get_reservation_link( if not sub_equipment_id: sub_equipment_id = "" url = f"https://{rec_area_domain_name}/create-booking/results" - query_params = { - "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) + 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(