from typing import Optional from .dates_is_leap_year import is_leap_year from .time_iso_to_unix import iso_to_unix DAY_NAMES = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") MONTH_DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) # Nine digits is 31 years; anything longer is a broken header, not a wait. MAX_DIGITS = 9 def _all_digits(text: str) -> bool: # A character range, not str.isdigit(), which accepts other scripts' digits. return len(text) > 0 and all("0" <= ch <= "9" for ch in text) def _imf_fixdate(t: str) -> Optional[int]: """An IMF-fixdate, "Sun, 06 Nov 1994 08:49:37 GMT", to Unix seconds; None if it is not one.""" if len(t) != 29: return None if t[0:3] not in DAY_NAMES or t[3:5] != ", " or t[7] != " " or t[11] != " " or t[16] != " ": return None if t[19] != ":" or t[22] != ":" or t[25:] != " GMT": return None if t[8:11] not in MONTHS: return None month = MONTHS.index(t[8:11]) dd, yyyy, hh, mi, ss = t[5:7], t[12:16], t[17:19], t[20:22], t[23:25] if not all(_all_digits(part) for part in (dd, yyyy, hh, mi, ss)): return None year, day = int(yyyy), int(dd) if year < 1 or int(hh) > 23 or int(mi) > 59 or int(ss) > 59: return None month_days = 29 if month == 1 and is_leap_year(year) else MONTH_DAYS[month] if day < 1 or day > month_days: return None return iso_to_unix("%s-%02d-%sT%s:%s:%sZ" % (yyyy, month + 1, dd, hh, mi, ss)) def retry_after_seconds(header: Optional[str], now: int) -> Optional[int]: """How long a client should wait before retrying, from a Retry-After header (RFC 9110 10.2.3): delay-seconds, or an HTTP date turned into seconds from `now`. A date in the past is 0. Anything else is None.""" if not isinstance(now, int) or isinstance(now, bool): raise ValueError("now must be a whole number of seconds") if not isinstance(header, str): return None # HTTP's optional whitespace is space and tab only (RFC 9110 5.6.3). t = header.strip(" \t") if _all_digits(t): return int(t) if len(t) <= MAX_DIGITS else None at = _imf_fixdate(t) if at is None: return None return at - now if at > now else 0