Skip to content

Commit b360db3

Browse files
authored
Merge pull request #10 from finos/develop
Added support for zoned datetime conversion.
2 parents 25a43a8 + 2ad877c commit b360db3

File tree

3 files changed

+100
-3
lines changed

3 files changed

+100
-3
lines changed

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ name = "rune.runtime"
77
dynamic = ["version"]
88
requires-python = ">=3.11"
99
dependencies = [
10-
"pydantic>=2.10.3"
10+
"pydantic>=2.10.3",
11+
"python-dateutil>=2.9.0.post0",
12+
"tzdata>=2025.2"
1113
]
1214
optional-dependencies.dev = [
1315
"pytest>=8.4.1",
1416
"pytest-cov>=6.2.1",
15-
"pytest-mock>=3.14.1"
17+
"pytest-mock>=3.14.1",
18+
"types-python-dateutil>=2.9.0.20250809"
1619
]
1720
description = "rune-runtime: the Rune DSL runtime for Python"
1821
readme = "README.md"

src/rune/runtime/utils.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
import logging
44
import keyword
55
import inspect
6+
import datetime
67
from enum import Enum
78
from typing import Callable, Any
9+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
10+
from dateutil import parser
811

912
__all__ = [
1013
'if_cond_fn', 'Multiprop', 'rune_any_elements', 'rune_get_only_element',
1114
'rune_filter', 'rune_all_elements', 'rune_contains', 'rune_disjoint',
1215
'rune_join', 'rune_flatten_list', 'rune_resolve_attr',
1316
'rune_resolve_deep_attr', 'rune_count', 'rune_attr_exists',
1417
'_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'
1620
]
1721

1822

@@ -133,6 +137,63 @@ def rune_str(x: Any) -> str:
133137
return str(x)
134138

135139

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+
136197
def _get_rune_object(base_model: str, attribute: str, value: Any) -> Any:
137198
model_class = globals()[base_model]
138199
instance_kwargs = {attribute: value}

test/test_zoned_date_time.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'''tests related to the zoned datetime conversion'''
2+
import pytest
3+
from rune.runtime.utils import rune_zoned_date_time as rdt
4+
5+
6+
def test_naive():
7+
'''no tz'''
8+
assert rdt("2025-01-02 03:04:05").tzinfo is None
9+
10+
11+
def test_offset_only():
12+
'''standard offset, no tz'''
13+
d = rdt("2025-01-02 03:04:05 +02:00")
14+
assert str(d.utcoffset()) == "2:00:00"
15+
16+
17+
def test_zone_only():
18+
'''tz present'''
19+
d = rdt("2025-01-02 03:04:05 Europe/Paris")
20+
assert d.tzinfo.key == "Europe/Paris" # type: ignore
21+
22+
23+
def test_offset_and_zone_match():
24+
'''tz and offset present'''
25+
rdt("2025-01-02 03:04:05 +01:00 Europe/Paris") # should not raise
26+
27+
28+
def test_offset_and_zone_mismatch():
29+
'''tz and offset present but do not match'''
30+
with pytest.raises(ValueError):
31+
rdt("2025-01-02 03:04:05 +03:00 Europe/Paris")
32+
33+
# EOF

0 commit comments

Comments
 (0)