Skip to content

Commit 5b6af54

Browse files
committed
Change mute_intervals to use exact dates with year (YYYY-MM-DD HH:MM)
Switches from MM-DD HH:MM (yearless) to YYYY-MM-DD HH:MM format. This simplifies the logic (no year-boundary wrapping needed) and gives users precise control over mute windows. https://claude.ai/code/session_01FGiv1N3QcFMWAPT4e1pu93
1 parent 552ae3e commit 5b6af54

4 files changed

Lines changed: 35 additions & 49 deletions

File tree

helm/robusta/values.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ playbookRepos: {}
1111

1212
# sinks configurations
1313
# Each sink supports an optional mute_intervals field (parallel to activity) that mutes
14-
# all notifications during specified date/time ranges. Format: MM-DD HH:MM (no year).
14+
# all notifications during specified date/time ranges. Format: YYYY-MM-DD HH:MM.
1515
# Example:
1616
# sinksConfig:
1717
# - slack_sink:
@@ -21,10 +21,10 @@ playbookRepos: {}
2121
# mute_intervals:
2222
# timezone: UTC
2323
# intervals:
24-
# - start_date: "12-24 00:00"
25-
# end_date: "12-26 23:59"
26-
# - start_date: "01-01 00:00"
27-
# end_date: "01-01 23:59"
24+
# - start_date: "2025-12-24 00:00"
25+
# end_date: "2025-12-26 23:59"
26+
# - start_date: "2026-01-01 00:00"
27+
# end_date: "2026-01-01 23:59"
2828
sinksConfig: []
2929

3030
# global parameters

src/robusta/core/sinks/sink_base_params.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,16 @@ def check_intervals(cls, intervals: List[ActivityInterval]):
5757
return intervals
5858

5959

60-
DATE_TIME_RE = re.compile(r"^\d{2}-\d{2} \d{2}:\d{2}$")
60+
DATE_TIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$")
6161

6262

6363
def check_date_time_format(value: str) -> str:
6464
if not DATE_TIME_RE.match(value):
65-
raise ValueError(f"invalid date-time: {value}. Expected format: MM-DD HH:MM")
66-
month, rest = value.split("-", 1)
67-
day, time_part = rest.split(" ", 1)
65+
raise ValueError(f"invalid date-time: {value}. Expected format: YYYY-MM-DD HH:MM")
66+
date_part, time_part = value.split(" ", 1)
67+
year, month, day = date_part.split("-")
6868
hour, minute = time_part.split(":")
69-
month, day, hour, minute = int(month), int(day), int(hour), int(minute)
69+
year, month, day, hour, minute = int(year), int(month), int(day), int(hour), int(minute)
7070
if not (1 <= month <= 12):
7171
raise ValueError(f"invalid month: {month}")
7272
if not (1 <= day <= 31):
@@ -79,8 +79,8 @@ def check_date_time_format(value: str) -> str:
7979

8080

8181
class MuteInterval(BaseModel):
82-
start_date: str # MM-DD HH:MM
83-
end_date: str # MM-DD HH:MM
82+
start_date: str # YYYY-MM-DD HH:MM
83+
end_date: str # YYYY-MM-DD HH:MM
8484

8585
_validator_start = validator("start_date", allow_reuse=True)(check_date_time_format)
8686
_validator_end = validator("end_date", allow_reuse=True)(check_date_time_format)

src/robusta/core/sinks/timing.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,36 +70,24 @@ def is_active_now(self) -> bool:
7070
class MuteDateInterval:
7171
"""Checks if the current date/time falls within a mute interval.
7272
73-
start_date and end_date are in MM-DD HH:MM format (no year).
74-
The interval applies to the current year. If start_date > end_date
75-
(e.g. 12-20 to 01-05), it wraps across the year boundary.
73+
start_date and end_date are in YYYY-MM-DD HH:MM format.
7674
"""
7775

7876
def __init__(self, start_date: str, end_date: str, timezone: str = "UTC"):
79-
self.start_month, self.start_day, self.start_hour, self.start_minute = self._parse(start_date)
80-
self.end_month, self.end_day, self.end_hour, self.end_minute = self._parse(end_date)
77+
self.start = self._parse(start_date)
78+
self.end = self._parse(end_date)
8179
try:
8280
self.timezone = pytz.timezone(timezone)
8381
except pytz.exceptions.UnknownTimeZoneError:
8482
raise ValueError(f"Unknown time zone {timezone}")
8583

86-
def _parse(self, date_str: str) -> Tuple[int, int, int, int]:
84+
def _parse(self, date_str: str) -> Tuple[int, int, int, int, int]:
8785
date_part, time_part = date_str.strip().split(" ")
88-
month, day = date_part.split("-")
86+
year, month, day = date_part.split("-")
8987
hour, minute = time_part.split(":")
90-
return int(month), int(day), int(hour), int(minute)
91-
92-
def _to_tuple(self, month: int, day: int, hour: int, minute: int) -> Tuple[int, int, int, int]:
93-
return (month, day, hour, minute)
88+
return int(year), int(month), int(day), int(hour), int(minute)
9489

9590
def is_muted_now(self) -> bool:
9691
now = datetime.now(self.timezone)
97-
current = self._to_tuple(now.month, now.day, now.hour, now.minute)
98-
start = self._to_tuple(self.start_month, self.start_day, self.start_hour, self.start_minute)
99-
end = self._to_tuple(self.end_month, self.end_day, self.end_hour, self.end_minute)
100-
101-
if start <= end:
102-
return start <= current <= end
103-
else:
104-
# Wraps across year boundary (e.g. 12-20 00:00 to 01-05 00:00)
105-
return current >= start or current <= end
92+
current = (now.year, now.month, now.day, now.hour, now.minute)
93+
return self.start <= current <= self.end

tests/test_sink_timing.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,22 @@ def test_unknown_timezone(self):
4848
"start_date,end_date,timezone,expected_muted",
4949
[
5050
# 2012-01-01 13:45 UTC - currently muted (within range)
51-
("01-01 00:00", "01-01 23:59", "UTC", True),
52-
# Currently muted (multi-day range)
53-
("12-31 00:00", "01-02 10:00", "UTC", True),
51+
("2012-01-01 00:00", "2012-01-01 23:59", "UTC", True),
52+
# Currently muted (multi-day range spanning year boundary)
53+
("2011-12-31 00:00", "2012-01-02 10:00", "UTC", True),
5454
# Not muted (range in February)
55-
("02-01 00:00", "02-28 23:59", "UTC", False),
56-
# Not muted (same day but hours don't match - before current time)
57-
("01-01 00:00", "01-01 13:00", "UTC", False),
55+
("2012-02-01 00:00", "2012-02-28 23:59", "UTC", False),
56+
# Not muted (same day but end is before current time)
57+
("2012-01-01 00:00", "2012-01-01 13:00", "UTC", False),
5858
# Muted (same day, hours match)
59-
("01-01 13:00", "01-01 14:00", "UTC", True),
60-
# Year-boundary wrap: Dec 20 to Jan 5 should mute on Jan 1
61-
("12-20 00:00", "01-05 23:59", "UTC", True),
62-
# Year-boundary wrap: March to Feb wraps around, Jan 1 IS inside that range
63-
("03-01 00:00", "02-15 23:59", "UTC", True),
64-
# Not muted: range is Feb 1 to Feb 28, Jan 1 is outside
65-
("02-01 00:00", "02-10 23:59", "UTC", False),
59+
("2012-01-01 13:00", "2012-01-01 14:00", "UTC", True),
60+
# Not muted (range is entirely in the past)
61+
("2011-06-01 00:00", "2011-06-30 23:59", "UTC", False),
62+
# Not muted (range is entirely in the future)
63+
("2013-01-01 00:00", "2013-12-31 23:59", "UTC", False),
6664
# Timezone test: 2012-01-01 13:45 UTC = 2012-01-01 14:45 CET
67-
("01-01 14:00", "01-01 15:00", "CET", True),
68-
("01-01 15:00", "01-01 16:00", "CET", False),
65+
("2012-01-01 14:00", "2012-01-01 15:00", "CET", True),
66+
("2012-01-01 15:00", "2012-01-01 16:00", "CET", False),
6967
],
7068
)
7169
def test_is_muted_now(self, start_date, end_date, timezone, expected_muted):
@@ -111,7 +109,7 @@ def test_accepts_muted(self):
111109
mock_registry = Mock(get_global_config=lambda: Mock())
112110
sink = _TestSinkBase(registry=mock_registry, sink_params=Mock())
113111
sink.time_slices = [TimeSlice(["sun"], [("13:30", "14:00")], "UTC")]
114-
sink.mute_date_intervals = [MuteDateInterval("01-01 00:00", "01-01 23:59", "UTC")]
112+
sink.mute_date_intervals = [MuteDateInterval("2012-01-01 00:00", "2012-01-01 23:59", "UTC")]
115113
mock_finding = Mock(matches=Mock(return_value=True))
116114
with freeze_time("2012-01-01 13:45"): # this is UTC time, Sunday
117115
# Would normally be accepted (Sunday 13:45 in 13:30-14:00), but muted
@@ -122,7 +120,7 @@ def test_accepts_not_muted(self):
122120
mock_registry = Mock(get_global_config=lambda: Mock())
123121
sink = _TestSinkBase(registry=mock_registry, sink_params=Mock())
124122
sink.time_slices = [TimeSlice(["sun"], [("13:30", "14:00")], "UTC")]
125-
sink.mute_date_intervals = [MuteDateInterval("02-01 00:00", "02-28 23:59", "UTC")]
123+
sink.mute_date_intervals = [MuteDateInterval("2012-02-01 00:00", "2012-02-28 23:59", "UTC")]
126124
mock_finding = Mock(matches=Mock(return_value=True))
127125
with freeze_time("2012-01-01 13:45"): # this is UTC time, Sunday
128126
# Mute is for February, so should still accept

0 commit comments

Comments
 (0)