Maintenance Window

When working with RDS maintenance operations it may be useful to see if a given date and time are within an instance's configured maintenance window. AWS use a fairly esoteric format to express these windows.

from datetime import datetime, timedelta
import sys
from typing import Dict, Tuple
import unittest


WEEKDAY_NUMBERS: Dict[str, int] = {
    "mon": 0,
    "tue": 1,
    "wed": 2,
    "thu": 3,
    "fri": 4,
    "sat": 5,
    "sun": 6,
}


class TestCase(unittest.TestCase):
    def test_parse_aws_datetime_range(self):
        start, end = parse_aws_datetime_range("sat:04:02-sat:04:32")
        assert start == ("sat", (4, 2))
        assert end == ("sat", (4, 32))

    def test_nearest_start_end_datetimes(self):
        assert nearest_start_end_datetimes(
            datetime(2021, 12, 25, 6, 0, 0), ("sat", (0, 0)), ("sun", (0, 0))
        ) == (
            datetime(2021, 12, 25, 0, 0, 0),
            datetime(2021, 12, 26, 0, 0, 0),
        )

    def test_nearest_start_end_datetimes_same_day(self):
        assert nearest_start_end_datetimes(
            datetime(2021, 11, 23, 9, 0, 0), ("sat", (4, 2)), ("sat", (4, 32))
        ) == (
            datetime(2021, 11, 20, 4, 2, 0),
            datetime(2021, 11, 20, 4, 32, 0),
        )

    def test_nearest_start_end_datetimes_across_weeks(self):
        assert nearest_start_end_datetimes(
            datetime(2021, 11, 23, 9, 0, 0), ("sun", (0, 0)), ("mon", (1, 0))
        ) == (
            datetime(2021, 11, 21, 0, 0, 0),
            datetime(2021, 11, 22, 1, 0, 0),
        )

def parse_aws_datetime_range(range: str) -> Tuple[Tuple[str, str]]:
    start, end = range.split("-", 1)
    start_day, start_hour, start_minute = start.split(":")
    end_day, end_hour, end_minute = end.split(":")

    return (
        (start_day, (int(start_hour), int(start_minute))),
        (end_day, (int(end_hour), int(end_minute))),
    )


def nearest_start_end_datetimes(
    time: datetime, start: Tuple[str, Tuple[int, int]], end: Tuple[str, Tuple[int, int]]
) -> Tuple[datetime, datetime]:
    start_day, start_time = start
    end_day, end_time = end

    date_weekday = time.weekday()
    start_day_offset = date_weekday - WEEKDAY_NUMBERS[start_day]
    if start_day_offset < 0:
        start_day_offset += 7
    start_datetime = time + timedelta(days=-start_day_offset)
    start_datetime = start_datetime.replace(
        hour=start_time[0], minute=start_time[1], second=0, microsecond=0
    )

    end_datetime = start_datetime
    while end_datetime.weekday() != WEEKDAY_NUMBERS[end_day]:
        end_datetime = start_datetime + timedelta(days=1)
    end_datetime = end_datetime.replace(
        hour=end_time[0], minute=end_time[1], second=0, microsecond=0
    )

    return start_datetime, end_datetime


if __name__ == "__main__":
    maintenance_window = sys.argv[1]

    now = datetime.now()
    start, end = parse_aws_datetime_range(maintenance_window)
    start_datetime, end_datetime = nearest_start_end_datetimes(now, start, end)
    if start_datetime <= now <= end_datetime:
        print(f"{now.isoformat()} is between {start_datetime.isoformat()} and {end_datetime.isoformat()}")
        sys.exit(0)
    else:
        print(f"{now.isoformat()} is not between {start_datetime.isoformat()} and {end_datetime.isoformat()}")
        sys.exit(1)