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