Skip to content

Commit c5e6e84

Browse files
committed
Updating to deal with optional start/end
1 parent e1d4a3a commit c5e6e84

File tree

3 files changed

+78
-62
lines changed

3 files changed

+78
-62
lines changed

featuremanagement/_time_window_filter/_models.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# -------------------------------------------------------------------------
66
import math
77
from enum import Enum
8-
from typing import Dict, Any, Optional
8+
from typing import Dict, Any, Optional, List
99
from datetime import datetime
1010
from dataclasses import dataclass
1111
from email.utils import parsedate_to_datetime
@@ -69,8 +69,8 @@ class RecurrencePattern: # pylint: disable=too-few-public-methods
6969
The recurrence pattern settings.
7070
"""
7171

72-
days: list = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
73-
days_of_week: list = []
72+
days: List[str] = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
73+
days_of_week: List[int] = []
7474

7575
def __init__(self, pattern_data: Dict[str, Any]):
7676
self.type = RecurrencePatternType.from_str(pattern_data.get("Type", "Daily"))
@@ -106,6 +106,9 @@ class Recurrence: # pylint: disable=too-few-public-methods
106106
The recurrence settings.
107107
"""
108108

109+
pattern: RecurrencePattern
110+
range: RecurrenceRange
111+
109112
def __init__(self, recurrence_data: Dict[str, Any]):
110113
self.pattern = RecurrencePattern(recurrence_data.get("Pattern", {}))
111114
self.range = RecurrenceRange(recurrence_data.get("Range", {}))
@@ -117,8 +120,8 @@ class TimeWindowFilterSettings:
117120
The settings for the time window filter.
118121
"""
119122

120-
start: datetime
121-
end: datetime
123+
start: Optional[datetime]
124+
end: Optional[datetime]
122125
recurrence: Optional[Recurrence]
123126

124127

featuremanagement/_time_window_filter/_recurrence_evaluator.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
# license information.
55
# -------------------------------------------------------------------------
66
from datetime import datetime, timedelta
7-
from typing import List, Optional
7+
from typing import Optional
88
from ._models import RecurrencePatternType, RecurrenceRangeType, TimeWindowFilterSettings, OccurrenceInfo
9-
from ._recurrence_validator import validate_settings
9+
from ._recurrence_validator import validate_settings, _get_passed_week_days, _sort_days_of_week
1010

1111
DAYS_PER_WEEK = 7
1212

13+
1314
def is_match(settings: TimeWindowFilterSettings, now: datetime) -> bool:
1415
"""
1516
Check if the current time is within the time window filter settings.
@@ -40,7 +41,7 @@ def _get_previous_occurrence(settings: TimeWindowFilterSettings, now: datetime)
4041
elif pattern_type == RecurrencePatternType.WEEKLY:
4142
occurrence_info = _get_weekly_previous_occurrence(settings, now)
4243
else:
43-
raise ValueError(f"Invalid recurrence pattern type: %s", pattern_type)
44+
raise ValueError(f"Invalid recurrence pattern type: {pattern_type}")
4445

4546
recurrence_range = settings.recurrence.range
4647
range_type = recurrence_range.type
@@ -97,7 +98,9 @@ def _get_weekly_previous_occurrence(settings: TimeWindowFilterSettings, now: dat
9798
day_with_min_offset = start
9899
if now < day_with_min_offset:
99100
most_recent_occurrence = (
100-
first_day_of_most_recent_occurring_week - timedelta(days=interval * DAYS_PER_WEEK) + timedelta(days=max_day_offset)
101+
first_day_of_most_recent_occurring_week
102+
- timedelta(days=interval * DAYS_PER_WEEK)
103+
+ timedelta(days=max_day_offset)
101104
)
102105
else:
103106
most_recent_occurrence = day_with_min_offset
@@ -113,19 +116,3 @@ def _get_weekly_previous_occurrence(settings: TimeWindowFilterSettings, now: dat
113116
num_of_occurrences += 1
114117

115118
return OccurrenceInfo(most_recent_occurrence, num_of_occurrences)
116-
117-
118-
def _get_passed_week_days(current_day: int, first_day_of_week: int) -> int:
119-
"""
120-
Get the number of days passed since the first day of the week.
121-
:param int current_day: The current day of the week (0-6).
122-
:param int first_day_of_week: The first day of the week (0-6).
123-
:return: The number of days passed since the first day of the week.
124-
:rtype: int
125-
"""
126-
return (current_day - first_day_of_week + DAYS_PER_WEEK) % DAYS_PER_WEEK
127-
128-
129-
def _sort_days_of_week(days_of_week: List[int], first_day_of_week: int) -> List[int]:
130-
sorted_days = sorted(days_of_week)
131-
return sorted_days[sorted_days.index(first_day_of_week) :] + sorted_days[: sorted_days.index(first_day_of_week)]

featuremanagement/_time_window_filter/_recurrence_validator.py

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
# license information.
55
# -------------------------------------------------------------------------
66
from datetime import datetime, timedelta
7-
from ._models import RecurrencePatternType, RecurrenceRangeType, TimeWindowFilterSettings
8-
from ._recurrence_evaluator import _get_passed_week_days, _sort_days_of_week
7+
from typing import List
8+
from ._models import RecurrencePatternType, RecurrenceRangeType, TimeWindowFilterSettings, Recurrence
99

1010

1111
DAYS_PER_WEEK = 7
@@ -27,13 +27,21 @@ def validate_settings(settings: TimeWindowFilterSettings) -> None:
2727
:param TimeWindowFilterSettings settings: The settings for the time window filter.
2828
:raises ValueError: If the settings are invalid.
2929
"""
30-
_validate_recurrence_required_parameter(settings)
31-
_validate_recurrence_pattern(settings)
32-
_validate_recurrence_range(settings)
30+
recurrence = settings.recurrence
31+
if recurrence is None:
32+
raise ValueError(REQUIRED_PARAMETER % "Recurrence")
3333

34+
start = settings.start
35+
end = settings.end
36+
if start is None or end is None:
37+
raise ValueError(REQUIRED_PARAMETER % "Start or End")
3438

35-
def _validate_recurrence_required_parameter(settings: TimeWindowFilterSettings) -> None:
36-
recurrence = settings.recurrence
39+
_validate_recurrence_required_parameter(recurrence, start, end)
40+
_validate_recurrence_pattern(recurrence, start, end)
41+
_validate_recurrence_range(recurrence, start)
42+
43+
44+
def _validate_recurrence_required_parameter(recurrence: Recurrence, start: datetime, end: datetime) -> None:
3745
param_name = ""
3846
reason = ""
3947
if recurrence.pattern is None:
@@ -42,86 +50,88 @@ def _validate_recurrence_required_parameter(settings: TimeWindowFilterSettings)
4250
if recurrence.range is None:
4351
param_name = f"{RECURRENCE_RANGE}"
4452
reason = REQUIRED_PARAMETER
45-
if not settings.end > settings.start:
53+
if not end > start:
4654
param_name = "end"
4755
reason = OUT_OF_RANGE
48-
if settings.end > settings.start + timedelta(days=TEN_YEARS):
56+
if end > start + timedelta(days=TEN_YEARS):
4957
param_name = "end"
5058
reason = TIME_WINDOW_DURATION_TEN_YEARS
5159

5260
if param_name:
5361
raise ValueError(reason % param_name)
5462

5563

56-
def _validate_recurrence_pattern(settings: TimeWindowFilterSettings) -> None:
57-
pattern_type = settings.recurrence.pattern.type
64+
def _validate_recurrence_pattern(recurrence: Recurrence, start: datetime, end: datetime) -> None:
65+
if recurrence is None:
66+
raise ValueError(REQUIRED_PARAMETER % "Recurrence")
67+
pattern_type = recurrence.pattern.type
5868

5969
if pattern_type == RecurrencePatternType.DAILY:
60-
_validate_daily_recurrence_pattern(settings)
70+
_validate_daily_recurrence_pattern(recurrence, start, end)
6171
else:
62-
_validate_weekly_recurrence_pattern(settings)
72+
_validate_weekly_recurrence_pattern(recurrence, start, end)
6373

6474

65-
def _validate_recurrence_range(settings: TimeWindowFilterSettings) -> None:
66-
range_type = settings.recurrence.range.type
75+
def _validate_recurrence_range(recurrence: Recurrence, start: datetime) -> None:
76+
range_type = recurrence.range.type
6777
if range_type == RecurrenceRangeType.END_DATE:
68-
_validate_end_date(settings)
78+
_validate_end_date(recurrence, start)
6979

7080

71-
def _validate_daily_recurrence_pattern(settings: TimeWindowFilterSettings) -> None:
81+
def _validate_daily_recurrence_pattern(recurrence: Recurrence, start: datetime, end: datetime) -> None:
7282
# "Start" is always a valid first occurrence for "Daily" pattern.
7383
# Only need to check if time window validated
74-
_validate_time_window_duration(settings)
84+
_validate_time_window_duration(recurrence, start, end)
7585

7686

77-
def _validate_weekly_recurrence_pattern(settings: TimeWindowFilterSettings) -> None:
78-
_validate_days_of_week(settings)
87+
def _validate_weekly_recurrence_pattern(recurrence: Recurrence, start: datetime, end: datetime) -> None:
88+
_validate_days_of_week(recurrence)
7989

8090
# Check whether "Start" is a valid first occurrence
81-
pattern = settings.recurrence.pattern
82-
if settings.start.weekday() not in pattern.days_of_week:
91+
pattern = recurrence.pattern
92+
if start.weekday() not in pattern.days_of_week:
8393
raise ValueError(NOT_MATCHED % "start")
8494

8595
# Time window duration must be shorter than how frequently it occurs
86-
_validate_time_window_duration(settings)
96+
_validate_time_window_duration(recurrence, start, end)
8797

8898
# Check whether the time window duration is shorter than the minimum gap between days of week
89-
if not _is_duration_compliant_with_days_of_week(settings):
99+
if not _is_duration_compliant_with_days_of_week(recurrence, start, end):
90100
raise ValueError(TIME_WINDOW_DURATION_OUT_OF_RANGE % "Recurrence.Pattern.DaysOfWeek")
91101

92102

93-
def _validate_time_window_duration(settings: TimeWindowFilterSettings) -> None:
94-
pattern = settings.recurrence.pattern
103+
def _validate_time_window_duration(recurrence: Recurrence, start: datetime, end: datetime) -> None:
104+
pattern = recurrence.pattern
95105
interval_duration = (
96106
timedelta(days=pattern.interval)
97107
if pattern.type == RecurrencePatternType.DAILY
98108
else timedelta(days=pattern.interval * DAYS_PER_WEEK)
99109
)
100-
time_window_duration = settings.end - settings.start
110+
time_window_duration = end - start
101111
if time_window_duration > interval_duration:
102112
raise ValueError(TIME_WINDOW_DURATION_OUT_OF_RANGE % "Recurrence.Pattern.Interval")
103113

104114

105-
def _validate_days_of_week(settings: TimeWindowFilterSettings) -> None:
106-
days_of_week = settings.recurrence.pattern.days_of_week
115+
def _validate_days_of_week(recurrence: Recurrence) -> None:
116+
days_of_week = recurrence.pattern.days_of_week
107117
if not days_of_week:
108118
raise ValueError(REQUIRED_PARAMETER % "Recurrence.Pattern.DaysOfWeek")
109119

110120

111-
def _validate_end_date(settings: TimeWindowFilterSettings) -> None:
112-
end_date = settings.recurrence.range.end_date
113-
if end_date and end_date < settings.start:
121+
def _validate_end_date(recurrence: Recurrence, start: datetime) -> None:
122+
end_date = recurrence.range.end_date
123+
if end_date and end_date < start:
114124
raise ValueError("The Recurrence.Range.EndDate should be after the Start")
115125

116126

117-
def _is_duration_compliant_with_days_of_week(settings: TimeWindowFilterSettings) -> bool:
118-
days_of_week = settings.recurrence.pattern.days_of_week
127+
def _is_duration_compliant_with_days_of_week(recurrence: Recurrence, start: datetime, end: datetime) -> bool:
128+
days_of_week = recurrence.pattern.days_of_week
119129
if len(days_of_week) == 1:
120130
return True
121131

122132
# Get the date of first day of the week
123133
today = datetime.now()
124-
first_day_of_week = settings.recurrence.pattern.first_day_of_week
134+
first_day_of_week = recurrence.pattern.first_day_of_week
125135
offset = _get_passed_week_days(today.weekday(), first_day_of_week)
126136
first_date_of_week = today - timedelta(days=offset)
127137
sorted_days_of_week = _sort_days_of_week(days_of_week, first_day_of_week)
@@ -137,7 +147,7 @@ def _is_duration_compliant_with_days_of_week(settings: TimeWindowFilterSettings)
137147
min_gap = min(min_gap, current_gap)
138148
prev_occurrence = date
139149

140-
if settings.recurrence.pattern.interval == 1:
150+
if recurrence.pattern.interval == 1:
141151
# It may cross weeks. Check the adjacent week
142152
date = first_date_of_week + timedelta(
143153
days=DAYS_PER_WEEK + _get_passed_week_days(sorted_days_of_week[0], first_day_of_week)
@@ -149,5 +159,21 @@ def _is_duration_compliant_with_days_of_week(settings: TimeWindowFilterSettings)
149159
current_gap = date - prev_occurrence
150160
min_gap = min(min_gap, current_gap)
151161

152-
time_window_duration = settings.end - settings.start
162+
time_window_duration = end - start
153163
return min_gap >= time_window_duration
164+
165+
166+
def _get_passed_week_days(current_day: int, first_day_of_week: int) -> int:
167+
"""
168+
Get the number of days passed since the first day of the week.
169+
:param int current_day: The current day of the week, where Monday == 0 ... Sunday == 6.
170+
:param int first_day_of_week: The first day of the week (0-6), where Monday == 0 ... Sunday == 6.
171+
:return: The number of days passed since the first day of the week.
172+
:rtype: int
173+
"""
174+
return (current_day - first_day_of_week + DAYS_PER_WEEK) % DAYS_PER_WEEK
175+
176+
177+
def _sort_days_of_week(days_of_week: List[int], first_day_of_week: int) -> List[int]:
178+
sorted_days = sorted(days_of_week)
179+
return sorted_days[sorted_days.index(first_day_of_week) :] + sorted_days[: sorted_days.index(first_day_of_week)]

0 commit comments

Comments
 (0)