From 23c705ce38ed25c75ec9fafae7f38ad33aa7451d Mon Sep 17 00:00:00 2001 From: Seth Girvan Date: Tue, 12 Sep 2023 19:01:05 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20campsite=20search=20by=20exac?= =?UTF-8?q?t=20date=20windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use with the `--exact-windows` flag to `camply campsites` --- camply/cli.py | 25 ++ camply/containers/search_model.py | 1 + camply/search/base_search.py | 177 ++++++++++++- camply/search/search_going_to_camp.py | 2 +- camply/utils/yaml_utils.py | 1 + docs/command_line_usage.md | 28 +++ docs/yaml_search.json | 5 + .../cassettes/test_search_exact_windows.yaml | 235 ++++++++++++++++++ tests/cli/test_campsites.py | 24 ++ 9 files changed, 486 insertions(+), 12 deletions(-) create mode 100644 tests/cli/cassettes/test_search_exact_windows.yaml diff --git a/camply/cli.py b/camply/cli.py index 7cb73fa4..f47e2d4f 100644 --- a/camply/cli.py +++ b/camply/cli.py @@ -494,6 +494,14 @@ def campgrounds( metavar="TEXT", help="Day(s) of the Week to search.", ) +exact_windows_argument = click.option( + "--exact-windows", + is_flag=True, + show_default=True, + default=False, + help="Search only for bookings which exactly match one of the ranges " + "specified with the --start-date and --end-date arguments.", +) def _get_equipment(equipment: Optional[List[str]]) -> List[Tuple[str, Optional[int]]]: @@ -526,7 +534,10 @@ def _validate_campsites( notify_first_try: bool, search_forever: bool, search_once: bool, + weekends: bool, + nights: int, day: Optional[Tuple[str]], + exact_windows: bool, **kwargs: Dict[str, Any], ) -> Tuple[bool, List[SearchWindow], Set[int]]: """ @@ -548,7 +559,10 @@ def _validate_campsites( notifications: List[str] notify_first_try: bool search_forever: bool + weekends: bool + nights: int day: Optional[Tuple[str]] + exact_windows: bool **kwargs: Dict[str, Any] Returns @@ -583,6 +597,11 @@ def _validate_campsites( "You cannot specify `--search-once` alongside `--continuous` or `--search-forever`" ) sys.exit(1) + if exact_windows and any([day, weekends, nights != 1]): + logger.error( + "You cannot specify `--exact-windows` alongside `--nights`, `--day`, or `--weekends`" + ) + sys.exit(1) if any( [ @@ -618,6 +637,7 @@ def _get_provider_kwargs_from_cli( equipment: Tuple[Union[str, int]], equipment_id: Tuple[Union[str, int]], day: Optional[Tuple[str]], + exact_windows: bool, ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ Get Provider kwargs from CLI @@ -640,6 +660,7 @@ def _get_provider_kwargs_from_cli( search_forever=search_forever, search_once=search_once, day=day, + exact_windows=exact_windows, ) if len(notifications) == 0: notifications = ["silent"] @@ -665,6 +686,7 @@ def _get_provider_kwargs_from_cli( "equipment": equipment, "equipment_id": equipment_id, "days_of_the_week": days_of_the_week, + "exact_windows": exact_windows, } search_kwargs = { "log": True, @@ -688,6 +710,7 @@ def _get_provider_kwargs_from_cli( @nights_argument @weekends_argument @day_of_the_week_argument +@exact_windows_argument @notifications_argument @continuous_argument @search_forever_argument @@ -725,6 +748,7 @@ def campsites( equipment: Tuple[Union[str, int]], equipment_id: Tuple[Union[str, int]], day: Optional[Tuple[str]], + exact_windows: bool, ) -> None: """ Find Available Campsites with Custom Search Criteria @@ -766,6 +790,7 @@ def campsites( equipment=equipment, equipment_id=equipment_id, day=day, + exact_windows=exact_windows, yaml_config=yaml_config, ) provider_class: Type[BaseCampingSearch] = CAMPSITE_SEARCH_PROVIDER[provider] diff --git a/camply/containers/search_model.py b/camply/containers/search_model.py index 34414e18..55e061ba 100644 --- a/camply/containers/search_model.py +++ b/camply/containers/search_model.py @@ -47,6 +47,7 @@ class YamlSearchFile(CamplyModel): days: Optional[List[str]] = None weekends: bool = False nights: int = 1 + exact_windows: bool = False continuous: bool = True polling_interval: int = SearchConfig.RECOMMENDED_POLLING_INTERVAL notifications: ArrayOrSingleStr = "silent" diff --git a/camply/search/base_search.py b/camply/search/base_search.py index 6a85b3a3..f9ecc77a 100644 --- a/camply/search/base_search.py +++ b/camply/search/base_search.py @@ -50,6 +50,7 @@ def __init__( offline_search: bool = False, offline_search_path: Optional[str] = None, days_of_the_week: Optional[Sequence[int]] = None, + exact_windows: bool = False, **kwargs, ) -> None: """ @@ -73,10 +74,18 @@ def __init__( When not specified, the filename will default to `camply_campsites.json` days_of_the_week: Optional[Sequence[int]] Days of the week (by weekday integer) to search for. + exact_windows: bool + When set to True, only availabilities exactly matching a passed + search_window will be returned. Useful when you have multiple search + windows with different numbers of nights, but only want to book a + given window if all days are available. """ self._verbose = kwargs.get("verbose", True) self.campsite_finder: ProviderType = self.provider_class() - self.search_window: List[SearchWindow] = make_list(search_window) + self.exact_windows: bool = exact_windows + self._original_search_windows: List[SearchWindow] = self._valid_search_windows( + make_list(search_window) + ) self.days_of_the_week = set( days_of_the_week if days_of_the_week is not None else () ) @@ -126,6 +135,13 @@ def search_days(self) -> List[datetime]: current_date = datetime.now().date() return [day for day in self._original_search_days if day >= current_date] + @property + def search_windows(self) -> List[SearchWindow]: + """ + Get the list of search windows that need to be searched + """ + return self._valid_search_windows(self._original_search_windows) + @property def search_months(self) -> List[datetime]: """ @@ -181,6 +197,31 @@ def _get_intersection_date_overlap( else: return False + def _has_matching_window( + self, + date: datetime, + periods: int, + ) -> bool: + """ + Determine if there is a matching search window when using exact_windows + + Parameters + ---------- + date: datetime + Start date of window to search for + periods: int + Number of days of window to search for + + Returns + ------- + bool + """ + return any( + window.start_date == date.date() + and (window.end_date - window.start_date).days == periods + for window in self.search_windows + ) + def _compare_date_overlap(self, campsite: AvailableCampsite) -> bool: """ See whether a campsite should be returned as found @@ -193,11 +234,17 @@ def _compare_date_overlap(self, campsite: AvailableCampsite) -> bool: ------- bool """ - intersection = self._get_intersection_date_overlap( - date=campsite.booking_date, - periods=campsite.booking_nights, - search_days=self.search_days, - ) + if self.exact_windows: + intersection = self._has_matching_window( + date=campsite.booking_date, + periods=campsite.booking_nights, + ) + else: + intersection = self._get_intersection_date_overlap( + date=campsite.booking_date, + periods=campsite.booking_nights, + search_days=self.search_days, + ) return intersection def _filter_date_overlap(self, campsites: DataFrame) -> pd.DataFrame: @@ -605,7 +652,7 @@ def _get_search_days(self) -> List[datetime]: """ current_date = datetime.now().date() search_nights = set() - for window in self.search_window: + for window in self.search_windows: generated_dates = { date for date in window.get_date_range() if date >= current_date } @@ -639,9 +686,30 @@ def _get_search_days(self) -> List[datetime]: raise RuntimeError(SearchConfig.ERROR_MESSAGE) return sorted(search_nights) - @classmethod + def _valid_search_windows(self, windows: List[SearchWindow]) -> List[SearchWindow]: + """ + Return the subset of windows which have not yet expired + + Parameters + ---------- + windows: List[SearchWindow] + + Returns + ------- + List[SearchWindow] + """ + current_date = datetime.now().date() + if self.exact_windows: + # In this case we are only interested if no days of the window have + # yet elapsed. + return [w for w in windows if w.start_date >= current_date] + else: + # In this case we are interested as long as there is still at least + # one day that has not yet elapsed. + return [w for w in windows if w.end_date >= current_date] + def _consolidate_campsites( - cls, campsite_df: DataFrame, nights: int + self, campsite_df: DataFrame, nights: int ) -> pd.DataFrame: """ Consolidate Single Night Campsites into Multiple Night Campsites @@ -679,14 +747,101 @@ def _consolidate_campsites( composed_grouping.drop( columns=[CampsiteContainerFields.CAMPSITE_GROUP], inplace=True ) - nightly_breakouts = cls._find_consecutive_nights( - dataframe=composed_grouping, nights=nights + nightly_breakouts = self._find_night_groupings( + dataframe=composed_grouping ) composed_groupings.append(nightly_breakouts) if len(composed_groupings) == 0: composed_groupings = [DataFrame()] return concat(composed_groupings, ignore_index=True) + def _find_night_groupings(self, dataframe: DataFrame) -> DataFrame: + """ + Find all matching night groupings in dataframe + + Matching criteria depends on the value of self.exact_windows. + + Parameters + ---------- + dataframe: DataFrame + + Returns + ------- + DataFrame + """ + if self.exact_windows: + return self._find_matching_windows(dataframe) + else: + return self._find_consecutive_nights(dataframe, self.nights) + + @staticmethod + def _booking_in_window(booking: Series, window: SearchWindow) -> bool: + """ + Return true only if the dates of booking are completely inside window + + Parameters + ---------- + booking: Series + AvailableCampsite converted to a Series + window: SearchWindow + + Returns + ------- + bool + """ + return ( + window.start_date <= booking["booking_date"].date() + and booking["booking_end_date"].date() <= window.end_date + ) + + def _find_matching_windows(self, dataframe: DataFrame) -> DataFrame: + """ + Find all sub sequences of dataframe that exactly match a search window + + Parameters + ---------- + dataframe: DataFrame + Each row contains a consecutive available night for the same + campsite + + Returns + ------- + DataFrame + """ + duplicate_subset = set(dataframe.columns) - AvailableCampsite.__unhashable__ + matching_windows = [] + for window in self.search_windows: + if ( + dataframe.booking_date.min().date() <= window.start_date + and window.end_date <= dataframe.booking_end_date.max().date() + ): + intersect_criteria = dataframe.apply( + self._booking_in_window, axis=1, window=window + ) + window_intersection = dataframe[intersect_criteria].copy() + + window_intersection.booking_date = ( + window_intersection.booking_date.min() + ) + window_intersection.booking_end_date = ( + window_intersection.booking_end_date.max() + ) + window_intersection.booking_url = window_intersection.booking_url.iloc[ + 0 + ] + window_intersection.booking_nights = ( + window_intersection.booking_end_date + - window_intersection.booking_date + ).dt.days + window_intersection.drop_duplicates( + inplace=True, subset=duplicate_subset + ) + matching_windows.append(window_intersection) + + if len(matching_windows) == 0: + matching_windows = [DataFrame()] + return concat(matching_windows, ignore_index=True) + @classmethod def _consecutive_subseq(cls, iterable: Iterable, length: int) -> Generator: """ diff --git a/camply/search/search_going_to_camp.py b/camply/search/search_going_to_camp.py index cb732fdf..1f90337f 100644 --- a/camply/search/search_going_to_camp.py +++ b/camply/search/search_going_to_camp.py @@ -138,7 +138,7 @@ def get_all_campsites(self) -> List[AvailableCampsite]: List[AvailableCampsite] """ available_sites = [] - for search_window in self.search_window: + for search_window in self.search_windows: current_start_date = search_window.get_current_start_date() for campground in self.campgrounds: sites = self.campsite_finder.list_site_availability( diff --git a/camply/utils/yaml_utils.py b/camply/utils/yaml_utils.py index 4d72b78a..9e2c5459 100644 --- a/camply/utils/yaml_utils.py +++ b/camply/utils/yaml_utils.py @@ -125,6 +125,7 @@ def yaml_file_to_arguments( "equipment": equipment, "offline_search": yaml_model.offline_search, "offline_search_path": yaml_model.offline_search_path, + "exact_windows": yaml_model.exact_windows, } search_kwargs = { "log": True, diff --git a/docs/command_line_usage.md b/docs/command_line_usage.md index bc622cd5..0dd58867 100644 --- a/docs/command_line_usage.md +++ b/docs/command_line_usage.md @@ -126,6 +126,10 @@ and a link to make the booking. Required parameters include `--start-date`, `--e - Search for campsite stays with consecutive nights. Defaults to 1 which returns all campsites found. [\*\*_example_](#look-for-consecutive-nights-at-the-same-campsite) +- `--exact-windows` + - Search only for bookings which exactly match one of the ranges specified with the `--start-date` and + `--end-date` arguments. + [\*\*_example_](#look-for-exact-date-ranges) - `--provider`: `PROVIDER` - Camping Search Provider. Defaults to 'RecreationDotGov', not case-sensitive. Options include: [RecreationDotGov](#searching-for-a-campsite), [Yellowstone](#look-for-a-campsite-inside-of-yellowstone), @@ -596,6 +600,30 @@ camply campsites \ --nights 4 ``` +### Look for Exact Date Ranges + +Sometimes you have multiple specific date ranges you are searching for that do not necessarily share the +same number of nights. Pass the `--exact-windows` flag to tell camply to only search for campsites with +stays that exactly match the one or more date ranges specified with `--start-date` and `--end-date`. + +The below example will search for two night availabilities starting 2023-09-08 and three night +availabilities starting 2023-09-15, but will not search for two night availabilities between 2023-09-15 and +2023-09-18. + +```commandline +camply campsites \ + --rec-area 2991 \ + --start-date 2023-09-08 \ + --end-date 2023-09-10 \ + --start-date 2023-09-15 \ + --end-date 2023-09-18 \ + --exact-windows +``` + +!!! note + + You cannot specify `--exact-windows` alongside `--nights`, `--day`, or `--weekends`. + ### Look for a Campsite Inside of Yellowstone Yellowstone doesn't use https://recreation.gov to manage its campgrounds, instead it uses its own diff --git a/docs/yaml_search.json b/docs/yaml_search.json index e7e743ac..6286c1f0 100644 --- a/docs/yaml_search.json +++ b/docs/yaml_search.json @@ -130,6 +130,11 @@ "default": 1, "type": "integer" }, + "exact_windows": { + "title": "Exact Windows", + "default": false, + "type": "boolean" + }, "continuous": { "title": "Continuous", "default": true, diff --git a/tests/cli/cassettes/test_search_exact_windows.yaml b/tests/cli/cassettes/test_search_exact_windows.yaml new file mode 100644 index 00000000..15afb759 --- /dev/null +++ b/tests/cli/cassettes/test_search_exact_windows.yaml @@ -0,0 +1,235 @@ +interactions: + - request: + body: null + headers: + User-Agent: + - Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) + Chrome/30.0.1599.17 Safari/537.36 + accept: + - application/json + apikey: + - REDACTED + method: GET + uri: https://ridb.recreation.gov/api/v1/facilities/232045?full=True + response: + body: + string: + "{\"ACTIVITY\":[{\"ActivityID\":5,\"ActivityName\":\"BIKING\",\"FacilityActivityDescription\":\"Biking\",\"FacilityActivityFeeDescription\":\"\",\"FacilityID\":\"232045\"},{\"ActivityID\":6,\"ActivityName\":\"BOATING\",\"FacilityActivityDescription\":\"Canoeing\",\"FacilityActivityFeeDescription\":\"\",\"FacilityID\":\"232045\"},{\"ActivityID\":9,\"ActivityName\":\"CAMPING\",\"FacilityActivityDescription\":\"Camping\",\"FacilityActivityFeeDescription\":\"Camping\",\"FacilityID\":\"232045\"},{\"ActivityID\":11,\"ActivityName\":\"FISHING\",\"FacilityActivityDescription\":\"Fishing\",\"FacilityActivityFeeDescription\":\"\",\"FacilityID\":\"232045\"},{\"ActivityID\":14,\"ActivityName\":\"HIKING\",\"FacilityActivityDescription\":\"Hiking\",\"FacilityActivityFeeDescription\":\"\",\"FacilityID\":\"232045\"},{\"ActivityID\":34,\"ActivityName\":\"SWIMMING + SITE\",\"FacilityActivityDescription\":\"Swimming\",\"FacilityActivityFeeDescription\":\"\",\"FacilityID\":\"232045\"}],\"CAMPSITE\":[],\"EVENT\":[],\"Enabled\":true,\"FACILITYADDRESS\":[{\"AddressCountryCode\":\"USA\",\"AddressStateCode\":\"CA\",\"City\":\"Foresthill\",\"FacilityAddressID\":\"20437400\",\"FacilityAddressType\":\"Default\",\"FacilityID\":\"232045\",\"FacilityStreetAddress1\":\"22830 + Foresthill Road\",\"FacilityStreetAddress2\":\"\",\"FacilityStreetAddress3\":\"\",\"LastUpdatedDate\":\"2023-09-12\",\"PostalCode\":\"95631\"}],\"FacilityAdaAccess\":\"N\",\"FacilityDescription\":\"

Overview

\\nForbes + Creek Group Campground is located near the southeast shore of Sugar Pine Reservoir + in the Tahoe National Forest. Visitors enjoy the area for its fishing, canoeing + and hiking opportunities. This facility is operated and maintained by the + Tahoe National Forest.

Recreation

\\nFishing, canoeing, swimming and + boating are popular activities on the reservoir. A trail system for walking + and non-motorized biking connects the two group sites at the campground, to + the lake and nearby boat ramp.\_\_ The North Fork of the American River is + nearby, offering opportunities for additional fishing and swimming. Many miles + of hiking, biking and off-road vehicle trails are in the surrounding area.

Facilities

\\nThe + campground offers two group campsites, Madrone and Rocky Ridge. Each site + can accommodate up to 50 people and 18 vehicles. Both sites are equipped with + a central cooking and picnic area, with a large campfire circle and multiple + tables.\_ RVs, trailers and tents are allowed within the campground. An unloading + area and accessible parking are available within the campground.

Natural + Features

\\nThe 160-acre Sugar Pine Reservoir sits at an elevation of + 3,600 feet. Stands of cedar and ponderosa pine provides ample shade in the + campground, which overlooks the reservoir. Summertime temperatures are warm + during the day and cool at night.\",\"FacilityDirections\":\"Take Interstate + 80 to the Foresthill/Auburn exit, near Auburn. Continue east for 17 miles + to Foresthill. Travel through town, about 9 miles, to Forest Road 10/Sugar + Pine Road. Turn left and continue about 9 miles. Look for the Sugar Pine Recreation + Area sign and turn right on the first road after the sign. Continue about + a half-mile to the Rocky Ridge Group Campground, on the right, and to the + Madrone Group Campground, at the end of the circle.\",\"FacilityEmail\":\"\",\"FacilityID\":\"232045\",\"FacilityLatitude\":39.1311111,\"FacilityLongitude\":-120.7819444,\"FacilityMapURL\":\"\",\"FacilityName\":\"FORBES + CREEK\",\"FacilityPhone\":\"530-367-2224\",\"FacilityReservationURL\":\"\",\"FacilityTypeDescription\":\"Campground\",\"FacilityUseFeeDescription\":\"\",\"GEOJSON\":{\"COORDINATES\":[-120.7819444,39.1311111],\"TYPE\":\"Point\"},\"Keywords\":\"FORB,TAHOE + NF - FS\",\"LINK\":[{\"Description\":\"California State Road Conditions\",\"EntityID\":\"232045\",\"EntityLinkID\":\"f7868acb9eddf3ff68c0365dc733c553\",\"EntityType\":\"Facility\",\"LinkType\":\"Other\",\"Title\":\"California + State Road Conditions\",\"URL\":\"http://www.dot.ca.gov\"},{\"Description\":\"California + State Tourism\",\"EntityID\":\"232045\",\"EntityLinkID\":\"60217acbf4be21db4ffa79eb82b5b513\",\"EntityType\":\"Facility\",\"LinkType\":\"Other\",\"Title\":\"California + State Tourism\",\"URL\":\"http://gocalif.ca.gov/AM/Template.cfm?Section=Home\"},{\"Description\":\"Print + Facility Map\",\"EntityID\":\"232045\",\"EntityLinkID\":\"7126b5c5f6110fa73149b633df8d09a0\",\"EntityType\":\"Facility\",\"LinkType\":\"Other\",\"Title\":\"Print + Facility Map\",\"URL\":\"http://www.recreation.gov/webphotos/facilitymaps/70266_FORB.pdf\"}],\"LastUpdatedDate\":\"2023-09-12\",\"LegacyFacilityID\":\"70266\",\"MEDIA\":[{\"Credits\":\"Joel + Miller USFS November 2018.\",\"Description\":\"Sugar pine\",\"EmbedCode\":\"\",\"EntityID\":\"232045\",\"EntityMediaID\":\"8bd74a9a-adaf-4c8b-b06e-b5e8610bfa61\",\"EntityType\":\"Facility\",\"Height\":340,\"IsGallery\":false,\"IsPreview\":false,\"IsPrimary\":true,\"MediaType\":\"Image\",\"Subtitle\":\"\",\"Title\":\"Sugar + pine\",\"URL\":\"https://cdn.recreation.gov/public/2018/11/15/00/31/232045_26f2c941-82f7-4a5f-9dee-0ab2cd6931e9_1440.jpg\",\"Width\":1440},{\"Credits\":\"\",\"Description\":\"\",\"EmbedCode\":\"\",\"EntityID\":\"232045\",\"EntityMediaID\":\"2570442\",\"EntityType\":\"Facility\",\"Height\":360,\"IsGallery\":true,\"IsPreview\":false,\"IsPrimary\":false,\"MediaType\":\"Image\",\"Subtitle\":\"\",\"Title\":\"FORBES + CREEK\",\"URL\":\"https://cdn.recreation.gov/public/images/63996.jpg\",\"Width\":540},{\"Credits\":\"\",\"Description\":\"\",\"EmbedCode\":\"\",\"EntityID\":\"232045\",\"EntityMediaID\":\"2570533\",\"EntityType\":\"Facility\",\"Height\":360,\"IsGallery\":true,\"IsPreview\":false,\"IsPrimary\":false,\"MediaType\":\"Image\",\"Subtitle\":\"\",\"Title\":\"FORBES + CREEK\",\"URL\":\"https://cdn.recreation.gov/public/images/64092.jpg\",\"Width\":540},{\"Credits\":\"\",\"Description\":\"\",\"EmbedCode\":\"\",\"EntityID\":\"232045\",\"EntityMediaID\":\"2570422\",\"EntityType\":\"Facility\",\"Height\":360,\"IsGallery\":true,\"IsPreview\":false,\"IsPrimary\":false,\"MediaType\":\"Image\",\"Subtitle\":\"\",\"Title\":\"FORBES + CREEK\",\"URL\":\"https://cdn.recreation.gov/public/images/63975.jpg\",\"Width\":540},{\"Credits\":\"\",\"Description\":\"\",\"EmbedCode\":\"\",\"EntityID\":\"232045\",\"EntityMediaID\":\"2570605\",\"EntityType\":\"Facility\",\"Height\":360,\"IsGallery\":true,\"IsPreview\":false,\"IsPrimary\":false,\"MediaType\":\"Image\",\"Subtitle\":\"\",\"Title\":\"FORBES + CREEK\",\"URL\":\"https://cdn.recreation.gov/public/images/64168.jpg\",\"Width\":540},{\"Credits\":\"\",\"Description\":\"\",\"EmbedCode\":\"\",\"EntityID\":\"232045\",\"EntityMediaID\":\"2570434\",\"EntityType\":\"Facility\",\"Height\":360,\"IsGallery\":true,\"IsPreview\":false,\"IsPrimary\":false,\"MediaType\":\"Image\",\"Subtitle\":\"\",\"Title\":\"FORBES + CREEK\",\"URL\":\"https://cdn.recreation.gov/public/images/63987.jpg\",\"Width\":540},{\"Credits\":\"\",\"Description\":\"\",\"EmbedCode\":\"\",\"EntityID\":\"232045\",\"EntityMediaID\":\"2570491\",\"EntityType\":\"Facility\",\"Height\":360,\"IsGallery\":true,\"IsPreview\":false,\"IsPrimary\":false,\"MediaType\":\"Image\",\"Subtitle\":\"\",\"Title\":\"FORBES + CREEK\",\"URL\":\"https://cdn.recreation.gov/public/images/64047.jpg\",\"Width\":540}],\"ORGANIZATION\":[{\"LastUpdatedDate\":\"2018-06-26\",\"OrgAbbrevName\":\"FS\",\"OrgID\":\"131\",\"OrgImageURL\":\"fs.jpg\",\"OrgJurisdictionType\":\"State\",\"OrgName\":\"USDA + Forest Service\",\"OrgParentID\":\"163\",\"OrgType\":\"Federal Agency\",\"OrgURLAddress\":\"http://www.fs.fed.us\",\"OrgURLText\":\"\"}],\"OrgFacilityID\":\"AN370266\",\"PERMITENTRANCE\":[],\"ParentOrgID\":\"131\",\"ParentRecAreaID\":\"1077\",\"RECAREA\":[{\"RecAreaID\":\"1077\",\"RecAreaName\":\"Tahoe + National Forest\",\"ResourceLink\":\"http://localhost:3000/api/v1/recareas/1077\"}],\"Reservable\":true,\"StayLimit\":\"\",\"TOUR\":[]}\n" + headers: + Access-Control-Allow-Origin: + - "*" + Alt-Svc: + - h3=":443"; ma=86400 + Connection: + - keep-alive + Content-Length: + - "7186" + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 13 Sep 2023 01:34:24 GMT + Server: + - CloudFront + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; always + Vary: + - Accept-Encoding + Via: + - 1.1 1343d20bdb50193b4d08099f66c57450.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - zTgdS6qOQF8sTJi1BNMevUOKxT42NugBWDCiRj6V63QSsslPSBoc8A== + X-Amz-Cf-Pop: + - SFO20-C1 + X-Cache: + - Miss from cloudfront + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate, br + Accept-Language: + - en-US,en;q=0.9,la;q=0.8 + Cache-Control: + - max-age=0 + Connection: + - keep-alive + Referer: + - https://www.recreation.gov/ + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, + like Gecko) Chrome/4.0.219.5 Safari/532.1 + method: GET + uri: https://www.recreation.gov/api/search/campsites?fq=asset_id%3A232045&include_non_site_specific_campsites=True&size=1000&start=0 + response: + body: + string: !!binary | + H4sIAAAAAAAAA+xX227jNhD9FULPsUVJvsVvXttdBN3EhuVkURSFQFOUzIaiVIpykgb59w5l2bG3 + dLZ1UqAF+iaRh3NmhofD4bNDSVaUXLPSGf787BBKWVnylWDO0NGqYs6FQ8qS6YjHMOIHPu5092OS + ZAb3w2zxaRqi8WI6/XE/p58KMzcG86nKKxmbGa0VX1V7st1vRIlmaa6eYIHxJYqZJlyUh0tqB/r9 + w5GGPlyTmB1BN0RUtWOVEM7LxTlMXd/CdE0e0U2VoTxBc5YXws7axWdyetjCOSYFoVw/uSH/naEF + 0VymVtoQJsCjM6k9G/Wa0Xsu0ZJn9kj9IcZofn1uinu2FAPf91PsnRumbVfrMPNKn47Te1+gva6F + dc50iUZC5A8stpL+BKfkJB/7reJFxqQ+nd3OCQHfsTWngqEvTKZ6baWGI/4OZpuKr0o03S1E10TG + RBtzNvK66rwn8ODtk9vEX9p3evAeat+m6IniG/ZAnlBYqYRQu8LmZAMyeA+1Leo99VTqE9n+ROh9 + 60qepgZLkmvO/to+m3KfcAWFCqqRPcXfQD6IGEKVUKtS9BUsKCvxN5APIv6suBD2SJupDyKacyo5 + RUuyOiXeY8QH0d6RSkBpzLmAgmWlPUa8/AKYDVMkZZHa3lbDoA0VcNdobDuJQTDoe87BqGIlUxu2 + 6xtCGGuFBaM84fQQV2qiK+gfnFnBpJmAu9Fc9TkY0GtIuBmDfgMEH9E8NrZuIVwWoxBW1kEnDGhY + Voj6f/jszBJz25B7Y7X5/LyY3c5RuBzdTEaLCbqZ3Uy/TMfLxdUYDDTY7+KWikhwmucS0K8/b675 + SsQ9ujILmq830C8XDsTAdVWHGVy2vQD3+t2+18MY+xisiVymu/mW5+N2f+D38KWHDQJvEXlhCuRo + sgDLMLDTdWi2R+aa06Zd0+xRmzjWvERmKxA0i3mW5bFJI+ohDfWpbMOiZgsfiJJm+40Q/+5aLpO8 + lpKsshVTUZ40YgJX4A7PVbqVkRcYL81v4/ZtOBmhrRhQCILidcEtiDLF09bJHk3ZG9ojSOMgPexr + C6YyrkFj0b5Q1xl7LduN4cUdoDPyGInt5TvsdE12/oRbGgvHSGwHKrgN6np3jIXMFYptOHuIeGaO + YqUEwNdaF+XQdWks24pRxYjRYzvNN25RrQSnro+9get5rtd1MXb9jlsf1Ih2OjhOaL816PSDVofg + oEW6rA+SosFlr+OxYOVHHoDavxamPd2eZlK/I8yFfuGYc8t2J3JMBE9ykAd53fI3T4XBGBFUpYHO + oL5Inq71tsj9u98s/0D39/8b6T/0RvoDAAD//+zXQQuCMBQH8K8S3Yd7e85lV4OIDoG3TjLbhEBU + 1I599556yjAIMhJ2G2OHvbf/g/2ckZxZZjWLeDNLziwfmCW61pNNfjm0dLcsiBNjOaCPTg5flwNw + GQhUpAKYkANuJIQcR3KIT9HxvIoPu/2THsQ/6ME5YT4nCEIC9wA9WnD0+rFMUlCGowpYqpRhfgaS + hVYbZg3tCwugQA5OqIqfOoEKbehz2OuxqWyeU7YSfWvLS1lThfQm622m82a4Qd3Frct4W7aaGiLu + DwAAAP//AwAOfQkyKRkAAA== + headers: + Access-Control-Allow-Origin: + - "*" + Age: + - "290" + Alt-Svc: + - h3=":443"; ma=86400 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 13 Sep 2023 01:29:34 GMT + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + Via: + - 1.1 7c6913fc3bfae6245d89d874d910fab4.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - iznXWj25LEbiumSdxwxtA-yQC9b9RS2zjnnjYuZm0Ezx9z5gkBBE6Q== + X-Amz-Cf-Pop: + - SFO53-P2 + X-Cache: + - Hit from cloudfront + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate, br + Accept-Language: + - en-US,en;q=0.9,la;q=0.8 + Cache-Control: + - max-age=0 + Connection: + - keep-alive + Referer: + - https://www.recreation.gov/ + Upgrade-Insecure-Requests: + - "1" + User-Agent: + - Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, + like Gecko) Chrome/4.0.210.0 Safari/532.0 + method: GET + uri: https://www.recreation.gov/api/camps/availability/campground/232045/month?start_date=2023-10-01T00%3A00%3A00.000Z + response: + body: + string: !!binary | + H4sIAAAAAAAAA+yWz2vCMBTH/xXpWSEv8Uf1VlREtumo7rBdSqbRBdra2VYmsv99STtmBR/vsoOK + 4KEk39rmNZ/Py8FZyChJdaZSp3dwXCGawl7IndShfNehznQ5xRkXDWANBnPGesXvzek5XhkMlVM/ + RjgdEXSkSUdadKR9EvFVqrY7tawmOmTCpR/TJSPA6AhdW6BrC4JaEDTJBF1ZaNORDh2hawtd6m05 + IxN0ZTldWU7vWk7vWk7XltO15eS25e75xHf9D/pAL81MSX1ldFtGg2yfKDM/M2ONWaIWeqUXJ7k8 + tHKI8zCsjP7eNfKnL8+12dybDDx/UJtMJ8PHYX/uj/vFXyRyobN9sJWZjtfFQ+J1scBws0ns6077 + D681fzwYDc1gJL+COI+CRG0Sk+q1mBnT8ckY1J3PXMYZoSzARAWYngCTEmAqAkxADPMOw3QDmGQA + UwtgQgFMI4DJg2HOYJgqABMEYFoATAaAKYBh5DMMeMAwBwxuwJAGDGTA8AUMWoaxWp3AvrnAvrk4 + Xbmh3VJpgZxxQ1KaJ4aWSMWZDAPLbIFfibAlN9isgjy1+elObWO9/sisMVzhduBqTwbnmvr9YHBj + B4OzXZKu7c0fDY6RM53fUn1hnf/JG/jmluvr+gyzy73r/0PXB4xqwFi+jK7/AwAA//9Cz4/IErh8 + TtVa35DcWh9UXOSX5pUoWRnVcgEAAAD//wMASZey+ioQAAA= + headers: + Access-Control-Allow-Origin: + - "*" + Alt-Svc: + - h3=":443"; ma=86400 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 13 Sep 2023 01:34:25 GMT + Server: + - CloudFront + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; always + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + Via: + - 1.1 060fd86e774e2e890f2f6a5bb72fc360.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - YV3LPBisFNSC3mAJQHxMDixCdtkpTf5yoZGb6UXV2JoAWjlhUAr0Bg== + X-Amz-Cf-Pop: + - SFO53-P2 + X-Cache: + - Miss from cloudfront + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cli/test_campsites.py b/tests/cli/test_campsites.py index ea2d27b1..bc1f0679 100644 --- a/tests/cli/test_campsites.py +++ b/tests/cli/test_campsites.py @@ -144,6 +144,30 @@ def test_search_nights(cli_runner: CamplyRunner) -> None: cli_status_checker(result=result, exit_code_zero=True) +@vcr_cassette +def test_search_exact_windows(cli_runner: CamplyRunner) -> None: + """ + Search Functionality: Exact Windows + """ + test_command = """ + camply \ + campsites \ + --campground 232045 + --start-date 2023-10-01 \ + --end-date 2023-10-05 \ + --start-date 2023-10-04 \ + --end-date 2023-10-08 \ + --start-date 2023-10-08 + --end-date 2023-10-10 \ + --exact-windows + """ + result = cli_runner.run_camply_command(command=test_command) + assert "Forbes Creek" in result.output + assert "9 booking nights selected for search" in result.output + assert "4 nights" in result.output + assert "2 nights" in result.output + + @vcr_cassette def test_search_yellowstone(cli_runner: CamplyRunner) -> None: """