@@ -50,6 +50,7 @@ def __init__(
50
50
offline_search : bool = False ,
51
51
offline_search_path : Optional [str ] = None ,
52
52
days_of_the_week : Optional [Sequence [int ]] = None ,
53
+ exact_windows : bool = False ,
53
54
** kwargs ,
54
55
) -> None :
55
56
"""
@@ -73,10 +74,18 @@ def __init__(
73
74
When not specified, the filename will default to `camply_campsites.json`
74
75
days_of_the_week: Optional[Sequence[int]]
75
76
Days of the week (by weekday integer) to search for.
77
+ exact_windows: bool
78
+ When set to True, only availabilities exactly matching a passed
79
+ search_window will be returned. Useful when you have multiple search
80
+ windows with different numbers of nights, but only want to book a
81
+ given window if all days are available.
76
82
"""
77
83
self ._verbose = kwargs .get ("verbose" , True )
78
84
self .campsite_finder : ProviderType = self .provider_class ()
79
- self .search_window : List [SearchWindow ] = make_list (search_window )
85
+ self .exact_windows : bool = exact_windows
86
+ self ._original_search_windows : List [SearchWindow ] = self ._valid_search_windows (
87
+ make_list (search_window )
88
+ )
80
89
self .days_of_the_week = set (
81
90
days_of_the_week if days_of_the_week is not None else ()
82
91
)
@@ -126,6 +135,13 @@ def search_days(self) -> List[datetime]:
126
135
current_date = datetime .now ().date ()
127
136
return [day for day in self ._original_search_days if day >= current_date ]
128
137
138
+ @property
139
+ def search_windows (self ) -> List [SearchWindow ]:
140
+ """
141
+ Get the list of search windows that need to be searched
142
+ """
143
+ return self ._valid_search_windows (self ._original_search_windows )
144
+
129
145
@property
130
146
def search_months (self ) -> List [datetime ]:
131
147
"""
@@ -181,6 +197,31 @@ def _get_intersection_date_overlap(
181
197
else :
182
198
return False
183
199
200
+ def _has_matching_window (
201
+ self ,
202
+ date : datetime ,
203
+ periods : int ,
204
+ ) -> bool :
205
+ """
206
+ Determine if there is a matching search window when using exact_windows
207
+
208
+ Parameters
209
+ ----------
210
+ date: datetime
211
+ Start date of window to search for
212
+ periods: int
213
+ Number of days of window to search for
214
+
215
+ Returns
216
+ -------
217
+ bool
218
+ """
219
+ return any (
220
+ window .start_date == date .date ()
221
+ and (window .end_date - window .start_date ).days == periods
222
+ for window in self .search_windows
223
+ )
224
+
184
225
def _compare_date_overlap (self , campsite : AvailableCampsite ) -> bool :
185
226
"""
186
227
See whether a campsite should be returned as found
@@ -193,11 +234,17 @@ def _compare_date_overlap(self, campsite: AvailableCampsite) -> bool:
193
234
-------
194
235
bool
195
236
"""
196
- intersection = self ._get_intersection_date_overlap (
197
- date = campsite .booking_date ,
198
- periods = campsite .booking_nights ,
199
- search_days = self .search_days ,
200
- )
237
+ if self .exact_windows :
238
+ intersection = self ._has_matching_window (
239
+ date = campsite .booking_date ,
240
+ periods = campsite .booking_nights ,
241
+ )
242
+ else :
243
+ intersection = self ._get_intersection_date_overlap (
244
+ date = campsite .booking_date ,
245
+ periods = campsite .booking_nights ,
246
+ search_days = self .search_days ,
247
+ )
201
248
return intersection
202
249
203
250
def _filter_date_overlap (self , campsites : DataFrame ) -> pd .DataFrame :
@@ -605,7 +652,7 @@ def _get_search_days(self) -> List[datetime]:
605
652
"""
606
653
current_date = datetime .now ().date ()
607
654
search_nights = set ()
608
- for window in self .search_window :
655
+ for window in self .search_windows :
609
656
generated_dates = {
610
657
date for date in window .get_date_range () if date >= current_date
611
658
}
@@ -639,9 +686,30 @@ def _get_search_days(self) -> List[datetime]:
639
686
raise RuntimeError (SearchConfig .ERROR_MESSAGE )
640
687
return sorted (search_nights )
641
688
642
- @classmethod
689
+ def _valid_search_windows (self , windows : List [SearchWindow ]) -> List [SearchWindow ]:
690
+ """
691
+ Return the subset of windows which have not yet expired
692
+
693
+ Parameters
694
+ ----------
695
+ windows: List[SearchWindow]
696
+
697
+ Returns
698
+ -------
699
+ List[SearchWindow]
700
+ """
701
+ current_date = datetime .now ().date ()
702
+ if self .exact_windows :
703
+ # In this case we are only interested if no days of the window have
704
+ # yet elapsed.
705
+ return [w for w in windows if w .start_date >= current_date ]
706
+ else :
707
+ # In this case we are interested as long as there is still at least
708
+ # one day that has not yet elapsed.
709
+ return [w for w in windows if w .end_date >= current_date ]
710
+
643
711
def _consolidate_campsites (
644
- cls , campsite_df : DataFrame , nights : int
712
+ self , campsite_df : DataFrame , nights : int
645
713
) -> pd .DataFrame :
646
714
"""
647
715
Consolidate Single Night Campsites into Multiple Night Campsites
@@ -679,14 +747,101 @@ def _consolidate_campsites(
679
747
composed_grouping .drop (
680
748
columns = [CampsiteContainerFields .CAMPSITE_GROUP ], inplace = True
681
749
)
682
- nightly_breakouts = cls . _find_consecutive_nights (
683
- dataframe = composed_grouping , nights = nights
750
+ nightly_breakouts = self . _find_night_groupings (
751
+ dataframe = composed_grouping
684
752
)
685
753
composed_groupings .append (nightly_breakouts )
686
754
if len (composed_groupings ) == 0 :
687
755
composed_groupings = [DataFrame ()]
688
756
return concat (composed_groupings , ignore_index = True )
689
757
758
+ def _find_night_groupings (self , dataframe : DataFrame ) -> DataFrame :
759
+ """
760
+ Find all matching night groupings in dataframe
761
+
762
+ Matching criteria depends on the value of self.exact_windows.
763
+
764
+ Parameters
765
+ ----------
766
+ dataframe: DataFrame
767
+
768
+ Returns
769
+ -------
770
+ DataFrame
771
+ """
772
+ if self .exact_windows :
773
+ return self ._find_matching_windows (dataframe )
774
+ else :
775
+ return self ._find_consecutive_nights (dataframe , self .nights )
776
+
777
+ @staticmethod
778
+ def _booking_in_window (booking : Series , window : SearchWindow ) -> bool :
779
+ """
780
+ Return true only if the dates of booking are completely inside window
781
+
782
+ Parameters
783
+ ----------
784
+ booking: Series
785
+ AvailableCampsite converted to a Series
786
+ window: SearchWindow
787
+
788
+ Returns
789
+ -------
790
+ bool
791
+ """
792
+ return (
793
+ window .start_date <= booking ["booking_date" ].date ()
794
+ and booking ["booking_end_date" ].date () <= window .end_date
795
+ )
796
+
797
+ def _find_matching_windows (self , dataframe : DataFrame ) -> DataFrame :
798
+ """
799
+ Find all sub sequences of dataframe that exactly match a search window
800
+
801
+ Parameters
802
+ ----------
803
+ dataframe: DataFrame
804
+ Each row contains a consecutive available night for the same
805
+ campsite
806
+
807
+ Returns
808
+ -------
809
+ DataFrame
810
+ """
811
+ duplicate_subset = set (dataframe .columns ) - AvailableCampsite .__unhashable__
812
+ matching_windows = []
813
+ for window in self .search_windows :
814
+ if (
815
+ dataframe .booking_date .min ().date () <= window .start_date
816
+ and window .end_date <= dataframe .booking_end_date .max ().date ()
817
+ ):
818
+ intersect_criteria = dataframe .apply (
819
+ self ._booking_in_window , axis = 1 , window = window
820
+ )
821
+ window_intersection = dataframe [intersect_criteria ].copy ()
822
+
823
+ window_intersection .booking_date = (
824
+ window_intersection .booking_date .min ()
825
+ )
826
+ window_intersection .booking_end_date = (
827
+ window_intersection .booking_end_date .max ()
828
+ )
829
+ window_intersection .booking_url = window_intersection .booking_url .iloc [
830
+ 0
831
+ ]
832
+ window_intersection .booking_nights = (
833
+ window_intersection .booking_end_date
834
+ - window_intersection .booking_date
835
+ ).dt .days
836
+ window_intersection .drop_duplicates (
837
+ inplace = True , subset = duplicate_subset
838
+ )
839
+ matching_windows .append (window_intersection )
840
+
841
+ if len (matching_windows ) == 0 :
842
+ matching_windows = [DataFrame ()]
843
+ return concat (matching_windows , ignore_index = True )
844
+
690
845
@classmethod
691
846
def _consecutive_subseq (cls , iterable : Iterable , length : int ) -> Generator :
692
847
"""
0 commit comments