Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 goingtocamp missing sites #293

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions camply/containers/goingtocamp.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 0 additions & 20 deletions camply/containers/gtc_api_responses.py

This file was deleted.

127 changes: 77 additions & 50 deletions camply/providers/going_to_camp/going_to_camp_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@
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
from pydantic import ValidationError

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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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]:
"""
Expand Down Expand Up @@ -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
Comment on lines +547 to +548
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we only use the equipmentCategoryId when subEquipmentCategoryId is also used.

Copy link
Contributor

@acaloiaro acaloiaro Sep 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the bug that "group sites" were ending up in the result set?

Assuming equipment_type_id is a non-group equipment type, the addition of the NON_GROUP_EQUIPMENT filter seems like it should be superfluous here.

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


Expand Down
4 changes: 2 additions & 2 deletions camply/search/search_going_to_camp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we allow sites with a maxCapacity, but not a minCapacity, to get selected

):
continue

Expand All @@ -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",
Expand Down
12 changes: 6 additions & 6 deletions tests/cli/cassettes/test_goingtocamp_equipment_types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -32,17 +32,17 @@ 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:
- strict-origin-when-cross-origin
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
Expand All @@ -61,7 +61,7 @@ interactions:
content-length:
- "1266"
x-azure-ref:
- 20230422T175830Z-whu0m374n55zbbqt6rxaenaw1s00000000v0000000009vud
- 20230831T153635Z-ttsvts4b5t27d9wfenxn58c1d800000001kg0000000047hm
status:
code: 200
message: OK
Expand Down
8 changes: 4 additions & 4 deletions tests/search_providers/test_goingtocamp_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}")
Expand Down
Loading