Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pendulum/parsing/__init__.py: 90%
103 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
1from __future__ import annotations
3import contextlib
4import copy
5import os
6import re
7import struct
9from datetime import date
10from datetime import datetime
11from datetime import time
12from typing import Any
13from typing import Optional
14from typing import cast
16from dateutil import parser
18from pendulum.parsing.exceptions import ParserError
21with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
23try:
24 if not with_extensions or struct.calcsize("P") == 4:
25 raise ImportError()
27 from _pendulum import Duration
28 from _pendulum import parse_iso8601
29except ImportError:
30 from pendulum.duration import Duration # type: ignore[assignment]
31 from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[assignment]
34COMMON = re.compile(
35 # Date (optional) # noqa: ERA001
36 "^"
37 "(?P<date>"
38 " (?P<classic>" # Classic date (YYYY-MM-DD)
39 r" (?P<year>\d{4})" # Year
40 " (?P<monthday>"
41 r" (?P<monthsep>[/:])?(?P<month>\d{2})" # Month (optional)
42 r" ((?P<daysep>[/:])?(?P<day>\d{2}))" # Day (optional)
43 " )?"
44 " )"
45 ")?"
46 # Time (optional) # noqa: ERA001
47 "(?P<time>" r" (?P<timesep>\ )?" # Separator (space)
48 # HH:mm:ss (optional mm and ss)
49 r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?"
50 # Subsecond part (optional)
51 " (?P<subsecondsection>"
52 " (?:[.|,])" # Subsecond separator (optional)
53 r" (?P<subsecond>\d{1,9})" # Subsecond
54 " )?"
55 ")?"
56 "$",
57 re.VERBOSE,
58)
60DEFAULT_OPTIONS = {
61 "day_first": False,
62 "year_first": True,
63 "strict": True,
64 "exact": False,
65 "now": None,
66}
69def parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration:
70 """
71 Parses a string with the given options.
73 :param text: The string to parse.
74 """
75 _options: dict[str, Any] = copy.copy(DEFAULT_OPTIONS)
76 _options.update(options)
78 return _normalize(_parse(text, **_options), **_options)
81def _normalize(
82 parsed: datetime | date | time | _Interval | Duration, **options: Any
83) -> datetime | date | time | _Interval | Duration:
84 """
85 Normalizes the parsed element.
87 :param parsed: The parsed elements.
88 """
89 if options.get("exact"):
90 return parsed
92 if isinstance(parsed, time):
93 now = cast(Optional[datetime], options["now"]) or datetime.now()
95 return datetime(
96 now.year,
97 now.month,
98 now.day,
99 parsed.hour,
100 parsed.minute,
101 parsed.second,
102 parsed.microsecond,
103 )
104 elif isinstance(parsed, date) and not isinstance(parsed, datetime):
105 return datetime(parsed.year, parsed.month, parsed.day)
107 return parsed
110def _parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration:
111 # Trying to parse ISO8601
112 with contextlib.suppress(ValueError):
113 return parse_iso8601(text)
115 with contextlib.suppress(ValueError):
116 return _parse_iso8601_interval(text)
118 with contextlib.suppress(ParserError):
119 return _parse_common(text, **options)
121 # We couldn't parse the string
122 # so we fallback on the dateutil parser
123 # If not strict
124 if options.get("strict", True):
125 raise ParserError(f"Unable to parse string [{text}]")
127 try:
128 dt = parser.parse(
129 text, dayfirst=options["day_first"], yearfirst=options["year_first"]
130 )
131 except ValueError:
132 raise ParserError(f"Invalid date string: {text}")
134 return dt
137def _parse_common(text: str, **options: Any) -> datetime | date | time:
138 """
139 Tries to parse the string as a common datetime format.
141 :param text: The string to parse.
142 """
143 m = COMMON.match(text)
144 has_date = False
145 year = 0
146 month = 1
147 day = 1
149 if not m:
150 raise ParserError("Invalid datetime string")
152 if m.group("date"):
153 # A date has been specified
154 has_date = True
156 year = int(m.group("year"))
158 if not m.group("monthday"):
159 # No month and day
160 month = 1
161 day = 1
162 else:
163 if options["day_first"]:
164 month = int(m.group("day"))
165 day = int(m.group("month"))
166 else:
167 month = int(m.group("month"))
168 day = int(m.group("day"))
170 if not m.group("time"):
171 return date(year, month, day)
173 # Grabbing hh:mm:ss
174 hour = int(m.group("hour"))
176 minute = int(m.group("minute"))
178 second = int(m.group("second")) if m.group("second") else 0
180 # Grabbing subseconds, if any
181 microsecond = 0
182 if m.group("subsecondsection"):
183 # Limiting to 6 chars
184 subsecond = m.group("subsecond")[:6]
186 microsecond = int(f"{subsecond:0<6}")
188 if has_date:
189 return datetime(year, month, day, hour, minute, second, microsecond)
191 return time(hour, minute, second, microsecond)
194class _Interval:
195 """
196 Special class to handle ISO 8601 intervals
197 """
199 def __init__(
200 self,
201 start: datetime | None = None,
202 end: datetime | None = None,
203 duration: Duration | None = None,
204 ) -> None:
205 self.start = start
206 self.end = end
207 self.duration = duration
210def _parse_iso8601_interval(text: str) -> _Interval:
211 if "/" not in text:
212 raise ParserError("Invalid interval")
214 first, last = text.split("/")
215 start = end = duration = None
217 if first[0] == "P":
218 # duration/end
219 duration = parse_iso8601(first)
220 end = parse_iso8601(last)
221 elif last[0] == "P":
222 # start/duration
223 start = parse_iso8601(first)
224 duration = parse_iso8601(last)
225 else:
226 # start/end
227 start = parse_iso8601(first)
228 end = parse_iso8601(last)
230 return _Interval(
231 cast(datetime, start), cast(datetime, end), cast(Duration, duration)
232 )
235__all__ = ["parse", "parse_iso8601"]