main
  1"""
  2This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py
  3without the Pydantic v1 specific errors.
  4"""
  5
  6from __future__ import annotations
  7
  8import re
  9from typing import Dict, Union, Optional
 10from datetime import date, datetime, timezone, timedelta
 11
 12from .._types import StrBytesIntFloat
 13
 14date_expr = r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
 15time_expr = (
 16    r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
 17    r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
 18    r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
 19)
 20
 21date_re = re.compile(f"{date_expr}$")
 22datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
 23
 24
 25EPOCH = datetime(1970, 1, 1)
 26# if greater than this, the number is in ms, if less than or equal it's in seconds
 27# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
 28MS_WATERSHED = int(2e10)
 29# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
 30MAX_NUMBER = int(3e20)
 31
 32
 33def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
 34    if isinstance(value, (int, float)):
 35        return value
 36    try:
 37        return float(value)
 38    except ValueError:
 39        return None
 40    except TypeError:
 41        raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None
 42
 43
 44def _from_unix_seconds(seconds: Union[int, float]) -> datetime:
 45    if seconds > MAX_NUMBER:
 46        return datetime.max
 47    elif seconds < -MAX_NUMBER:
 48        return datetime.min
 49
 50    while abs(seconds) > MS_WATERSHED:
 51        seconds /= 1000
 52    dt = EPOCH + timedelta(seconds=seconds)
 53    return dt.replace(tzinfo=timezone.utc)
 54
 55
 56def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]:
 57    if value == "Z":
 58        return timezone.utc
 59    elif value is not None:
 60        offset_mins = int(value[-2:]) if len(value) > 3 else 0
 61        offset = 60 * int(value[1:3]) + offset_mins
 62        if value[0] == "-":
 63            offset = -offset
 64        return timezone(timedelta(minutes=offset))
 65    else:
 66        return None
 67
 68
 69def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
 70    """
 71    Parse a datetime/int/float/string and return a datetime.datetime.
 72
 73    This function supports time zone offsets. When the input contains one,
 74    the output uses a timezone with a fixed offset from UTC.
 75
 76    Raise ValueError if the input is well formatted but not a valid datetime.
 77    Raise ValueError if the input isn't well formatted.
 78    """
 79    if isinstance(value, datetime):
 80        return value
 81
 82    number = _get_numeric(value, "datetime")
 83    if number is not None:
 84        return _from_unix_seconds(number)
 85
 86    if isinstance(value, bytes):
 87        value = value.decode()
 88
 89    assert not isinstance(value, (float, int))
 90
 91    match = datetime_re.match(value)
 92    if match is None:
 93        raise ValueError("invalid datetime format")
 94
 95    kw = match.groupdict()
 96    if kw["microsecond"]:
 97        kw["microsecond"] = kw["microsecond"].ljust(6, "0")
 98
 99    tzinfo = _parse_timezone(kw.pop("tzinfo"))
100    kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
101    kw_["tzinfo"] = tzinfo
102
103    return datetime(**kw_)  # type: ignore
104
105
106def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
107    """
108    Parse a date/int/float/string and return a datetime.date.
109
110    Raise ValueError if the input is well formatted but not a valid date.
111    Raise ValueError if the input isn't well formatted.
112    """
113    if isinstance(value, date):
114        if isinstance(value, datetime):
115            return value.date()
116        else:
117            return value
118
119    number = _get_numeric(value, "date")
120    if number is not None:
121        return _from_unix_seconds(number).date()
122
123    if isinstance(value, bytes):
124        value = value.decode()
125
126    assert not isinstance(value, (float, int))
127    match = date_re.match(value)
128    if match is None:
129        raise ValueError("invalid date format")
130
131    kw = {k: int(v) for k, v in match.groupdict().items()}
132
133    try:
134        return date(**kw)
135    except ValueError:
136        raise ValueError("invalid date format") from None