|
3 | 3 | import logging |
4 | 4 | import keyword |
5 | 5 | import inspect |
| 6 | +import datetime |
6 | 7 | from enum import Enum |
7 | 8 | from typing import Callable, Any |
| 9 | +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError |
| 10 | +from dateutil import parser |
8 | 11 |
|
9 | 12 | __all__ = [ |
10 | 13 | 'if_cond_fn', 'Multiprop', 'rune_any_elements', 'rune_get_only_element', |
11 | 14 | 'rune_filter', 'rune_all_elements', 'rune_contains', 'rune_disjoint', |
12 | 15 | 'rune_join', 'rune_flatten_list', 'rune_resolve_attr', |
13 | 16 | 'rune_resolve_deep_attr', 'rune_count', 'rune_attr_exists', |
14 | 17 | '_get_rune_object', 'rune_set_attr', 'rune_add_attr', |
15 | | - 'rune_check_cardinality', 'rune_str', 'rune_check_one_of' |
| 18 | + 'rune_check_cardinality', 'rune_str', 'rune_check_one_of', |
| 19 | + 'rune_zoned_date_time' |
16 | 20 | ] |
17 | 21 |
|
18 | 22 |
|
@@ -133,6 +137,63 @@ def rune_str(x: Any) -> str: |
133 | 137 | return str(x) |
134 | 138 |
|
135 | 139 |
|
| 140 | +def rune_zoned_date_time(dt_str: str) -> datetime.datetime: |
| 141 | + """ |
| 142 | + Return a `datetime` parsed from *dt_str*. |
| 143 | +
|
| 144 | + The input may contain: |
| 145 | + • a bare date-time, |
| 146 | + • a numeric UTC offset (e.g. “+02:00”), |
| 147 | + • an IANA time-zone name (e.g. “Europe/Paris”), or |
| 148 | + • both offset **and** zone (the offset is validated against the zone). |
| 149 | +
|
| 150 | + Parameters |
| 151 | + ---------- |
| 152 | + dt_str : str |
| 153 | + Date/time string such as |
| 154 | + “2024-06-01 12:34:56 +05:30 Asia/Kolkata”. |
| 155 | +
|
| 156 | + Returns |
| 157 | + ------- |
| 158 | + datetime.datetime |
| 159 | + Time-zone-aware when an offset or zone is supplied, otherwise naive. |
| 160 | +
|
| 161 | + Raises |
| 162 | + ------ |
| 163 | + ValueError |
| 164 | + If the supplied offset contradicts the IANA zone. |
| 165 | +
|
| 166 | + Notes |
| 167 | + ----- |
| 168 | + Parsing is delegated to `dateutil.parser.parse`; any of its |
| 169 | + `ParserError`s propagate unchanged for malformed date fragments. |
| 170 | + """ |
| 171 | + dt: datetime.datetime |
| 172 | + extras: list[Any] |
| 173 | + dt, extras = parser.parse(dt_str, fuzzy_with_tokens=True) # type: ignore |
| 174 | + |
| 175 | + # Try every leftover token (strip commas/periods) for a ZoneInfo name |
| 176 | + zone = None |
| 177 | + for tok in extras: |
| 178 | + tok = tok.strip(' ,.;') |
| 179 | + if not tok: |
| 180 | + continue |
| 181 | + try: |
| 182 | + if tok: |
| 183 | + zone = ZoneInfo(tok) |
| 184 | + break |
| 185 | + except ZoneInfoNotFoundError: |
| 186 | + continue |
| 187 | + |
| 188 | + if zone: |
| 189 | + # Same offset-matching logic as before … |
| 190 | + if dt.tzinfo and dt.utcoffset() != dt.astimezone(zone).utcoffset(): |
| 191 | + raise ValueError("Offset mismatch …") |
| 192 | + dt = dt.astimezone(zone) if dt.tzinfo else dt.replace(tzinfo=zone) |
| 193 | + |
| 194 | + return dt |
| 195 | + |
| 196 | + |
136 | 197 | def _get_rune_object(base_model: str, attribute: str, value: Any) -> Any: |
137 | 198 | model_class = globals()[base_model] |
138 | 199 | instance_kwargs = {attribute: value} |
|
0 commit comments